golang开发相关面试题

news2024/10/2 12:27:18

目录

go有哪些数据类型?

方法与函数有什么区别?

方法中值接收者与指针接收者的区别是什么?

函数返回局部变量的指针是否安全?

函数参数传递值是值传递还是引用传递?

defer关键字的实现原理?

内置函数make和new的区别?

slice底层实现原理?

array与slice的区别是什么?

slice深拷贝和浅拷贝

slice扩容机制是什么?

slice为什么不是线程安全的.

map的底层实现原理

为什么map遍历是无序的.

map为什么是非线程安全的

map如何查找

map冲突的解决方式

什么是负载因子?map的负载因子为什么是6.5?

map如何扩容

map和sync.Map谁的性能最好,为什么?

channel有什么特点

channel的底层实现原理是什么?

channelyou无缓冲的区别

channel为什么是线程安全的?

channel如何控制goroutine并发执行程序

channe共享内存有什么优劣势

如何那种情况下channel会出现死锁现象?应该怎么解决?或者预防死锁出现?

Go互斥锁的实现原理

什么是自旋?

互斥锁正常模式和饥饿模式的区别

Go 读写锁的实现原理


1.go有哪些数据类型?

  1. 布尔型 bool
  2. 数字类型 uint int float32 float64 byte rune
  3. 字符串类型  string
  4. 复合类型 数组类型 (array) 切片类型(slice) 字典类型(map) 管道类型(channel) 结构化类型(structure)
  5. 指针类型 pointer
  6. 接口类型 interface{}
  7. 函数类型 func
  8. 方法类型 method

2.方法与函数有什么区别?

  1. 函数是值不属于任何结构体,类型的方法,也就是说函数是没有接受者的,方法是有指定的接收者

3.方法中值接收者与指针接收者的区别是什么?

  1. 如果方法的接收者是指针,无论调用者是对象还是对象指针,修改的都是对象本身,惠永祥调用者,
  2. 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者本身
  3. (指针接收者会造成变量逃逸现象,并且会将变量分配到堆中.需要GC才能进行内存回收.)

4.函数返回局部变量的指针是否安全?

  1. 一般来说局部变量会在函数返回后直接被销毁,所以在函数返回后该变量就变成了无所知的引用.程序会进入未知状态.但是这在Go中是安全的.Go编译器会对每个局部变量进行逃逸分析.如果发现局部变量的作用域超过该函数.则不会将内存分配到栈上,而是分配到堆上,因为他们不在栈区,即使释放函数其内容本身也不会受影响.

5.函数参数传递值是值传递还是引用传递?

  1. Go语言中所有的传参都是值传递,都是一个副本,一个拷贝.
  2. 参数如果是非引用类型(int string struct等类型的话),这样的话在函数中就无法修改原内容数据,如果是引用类型的话(指针,map,channel,slice等) 这样的就可以修改原内容数据.(个人理解,值传递可以理解成一个函数内部的局部变量.这个变量无法跳出函数本身.所以当该类型的参数传递进函数后就是拷贝了原本的值.只能是函数内部使用.引用类型需要指定一个唯一的内存地址.这个地址上存储的是对应的值.在使用参数的时候需要将这个内存地址传递到函数中.因为内存地址是唯一的.所以无论在函数内部还是函数外部只要是修改,那么都会将原来的值进行修改.类似于全局变量,局部引用的效果.)

6.defer关键字的实现原理?

  1. defer是在return之前执行的.return并不是原子性操作. 函数返回过程是这样的:先给返回值然后调用defer表达式.最后返回到函数中.
  2. defer表达式可以在函数返回之后,在返回到调用函数之前修改返回值.使最终的返回值与你相像的不一样.

将defer的使用过程改写一下就可以更清晰的了解defer的使用规则:

返回值=xxx

调用defer 函数

空return
  • defer 关键字的实现跟 go 关键字很类似, 不同的是它调用的是 runtime.deferproc 而不是 runtime.newproc。在 defer 出现的地方, 插入了指令 call runtime.deferproc, 然后在函数返回之前的地方, 插入指令 call runtime.deferreturn。

普通的函数返回时, 汇编代码类似:

add xx SP
return

包含defer的语句则汇编代码是:

call runtime.deferreturn, 
add xx SP
return
  • goroutine 的控制结构中, 有一张表记录 defer, 调用 runtime.deferproc 时会将需要 defer 的表达式记录在表中, 而在调用 runtime.deferreturn 的时候, 则会依次从 defer 表中出栈并执行。defer关键字的原理

7.内置函数make和new的区别?

变量初始化一般分为两步, 变量声明+变量内存分配,var 关键字就是用来声明变量的,new和make主要用来分配内存的.

make只能用来分配初始化类型为slice.map.chan类型的数据.并且返回类型为类型本身.

new可以分配任意类型的数据,并且置零,返回一个指向该类型内存地址的指针.

8.slice底层实现原理?

切片是基于数组实现的.他的底层是数组,他自己本身非常小.可以理解为对底层数组的抽象,因为是基于底层数组的实现,所以他的底层内存结构是连续的.效率非常高.还可以通过索引获取数据,

切片本身并不是动态数组或者数组指针,它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写限定在指定的区域内,且本身只是一个只读对象,其工作机制类似于数组指针的一种封装(切片扩容,低于1024的情况下翻倍扩容,高于1024的情况下1.25倍原数组数量扩容.直到扩容的到所需求的大小为止,在扩容期间底层的内存地址是发生变化的.每扩容一次内存地址就会相应的改变一次.go数组扩容机制)

9.array与slice的区别是什么?

数据长度不同,

  1. 数组初始化必须指定长度,并且长度固定不可变.
  2. 切片长度不是固定的,可以追加元素,再追加时可能使切片的容量增大.

函数传参不同

  1. 数组是值类型,将一个数组赋值给另一个数组的时候传递的是一份深拷贝,函数传参操作会复制整个数组的数据,会占用额外的内存,函数对数组元素值的修改,不会修改原数组内容.(因为是值拷贝,所以原数组和函数内部的数组可以理解成是两个不同的数组.但是内部的值是相同的.所以函数内外看似是对同一个数组操作,实际上是值拷贝操作.)
  2. 切片是引用类型.将一个切片赋值给另一个切片时,传递的是一份浅拷贝.函数传参操作不会拷贝整个切片.只会赋值len和cap.底层共用一个数组.不会占用额外的内存.函数内对数组元素值的修改,会将函数外的数组的值一同修改.

计算数组长度方式不同

  1. 数组需要遍历计算数组长度,时间复杂度为O(n),
  2. 切片底层包含一个len字段.可以直接通过该字段的值来知道切片的长度.时间复杂度为O(1).

10.slice深拷贝和浅拷贝

  1. 深拷贝:拷贝的是数据本身,创造一个新对象,分配一个新内存地址,新对象与原有的对象不共享内存,修改新对象的值不会影响原对象的数据.同理修改原对象的值不会影响新对象的数据.
  2. 浅拷贝:拷贝的是数据地址,只复制指向数据对象的指针.此时新对象和老对象指向内存的位置是一样的.新对象值修改时老对象也会变化.

11.slice扩容机制是什么?

扩容会发生在slice append的时候.当slice的cap不足以容纳新增成员的时候.那么slice 就会进行扩容.扩容规则如下:

  1. 如果新申请容量比两倍的原容量大,那么扩容后容量为新申请容量.
  2. 如果原有slice长度小于1024,那么每次扩容是原来容量的2倍.
  3. 如果原slice长度大于1024,那么每次扩容是原让那个了的1.25倍.
  4. 如果最终容量计算值溢出,则最终容量就是新申请荣联.(超出系统可分配内存的情况下.)
  5. .每次的扩容伴随着内存地址变更.

12.slice为什么不是线程安全的.

  1. slice底层结构并没有使用加锁等方式,并不支持并发读写.所以并不是线程安全的.使用多个goroutine对类型为slice进行操作的时候,每次输出的值大概率都不会是一样的.因为slice底层是没有加锁的.所以会导致多个不同的goroutine读到相同下标进行操作.

13.map的底层实现原理

  1. Go中map是一个指针,占用8个字节,指向hmap结构体
  2. 源码包中src/runtime/map.go定义了hmap的数据结构:
  3. hmap包含若干个结构为bmap的数组.每个bmap底层都采用连表结构,bmap通常叫期bucket.
  • 补充(map实现原理Go map实现原理),,过程是key通过hash计算找到对应的bucket,然后通过bucket寻找其对应的值,同一个bucket只能存储8个键值对,超过这个数量后就会扩容.如果出现hash冲突的情况,那么最底层的bucket是一个8位有序数组,遍历这个数组最终找到相应的值.这种方法叫做开放地址法.(个人查找部分资料总结的,如果分析不对的欢迎指出.非常感谢.)

14.为什么map遍历是无序的.

主要原因有2点:

  • Go 语言中,当我们对 map进行遍历 时,并不是固定地从第一个数开始遍历,每次都是从随机的一个位置开始遍历。即使是一个不会改变的的 map,仅仅只是遍历它,也不太可能会返回一个固定顺序了
  • Go的map遍历结果无需,本质上收到两个方面的影响:"无需写入和扩容的影响"
  • 无序写入可以分成两种情况.
  1. 1.正常写入,(非哈希冲突写入):虽然buckets是一块连续的内存,但是每次写入都会通过hash到某一个bucket上,而不是按照buckets顺序写入,
  2. 2.哈希冲突写入,如果存在hash冲突的情况.那么数据就会写在同一个bucket上,因为每个bucket只能存放8对键值对.所以当超过这个数量的时候就会在创建一个bucket.然后对应的key会指向新的bucket.map的扩容会将原有所有的key都迁移到新的地址上.所以在map遇到扩容之后对应的内存地址会发生变化.所以无法做到有序.
  3. map 本身是无序的,且遍历时顺序还会被随机化,如果想顺序遍历 map,需要对 map key 先排序,再按照 key 的顺序遍历 map。

15.map为什么是非线程安全的

  • 在数据插入的时候
  1. 加入AB两个协程同事对同一个map进行操作.然后计算出了相同的哈希值对应想用的bucket数组位置,因为此时应该位置还没有数据,所以两个协成对同一个数组的头部位置进行数据写入,当A写入完成后,B再次进行写入到同一个位置.那么就会导致A的数据被B覆盖.从而导致A的数据丢失.
  • 在map扩容的时候,
  1. hashmap有个扩容操作.在这个操作后会生成一个新容量大的数组,然后对原数组的所有键值对重新写入到新的数组中,之后将原来的key指向新的数数组所对应的内存地址.那么当多线程操作的时候就会出现地址冲突的情况.结果就是只有最后一个线程被赋予给正确的值.其余的均会丢失.引用(为什么HashMap是非线程安全的?)

16.map如何查找

Go 语言中读取 map 有两种语法:带 comma 和 不带 comma。当要查询的 key 不在 map 里,带 comma 的用法会返回一个 bool 型变量提示 key 是否在 map 中;而不带 comma 的语句则会返回一个 value 类型的零值。如果 value 是 int 型就会返回 0,如果 value 是 string 类型,就会返回空字符串。

// 不带 comma 用法
value := m["name"]
fmt.Printf("value:%s", value)

// 带 comma 用法
value, ok := m["name"]
if ok {
    fmt.Printf("value:%s", value)
}

17.map冲突的解决方式

比较常用的Hash冲突解决方案有链地址法和开放寻址法:

  • 链地址法
  1. 当哈希冲突发生时,创建新单元,并将新单元添加到冲突单元所在链表的尾部。hash表存储所有想用记录所在连表的头指针.意思就是根据key找到对应的value,如果value是对应的头指针,那么表示该map是冲突的.然后根据指针找到对应的连表,连表中存储的是key,和value,比对查找的key就可以将对应的value拿到.
  • 开放寻址法
  1. 当哈希冲突发生时,从发生冲突的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。开放寻址法需要的表长度要大于等于所需要存放的元素数量(引用开放地址法开放地址法)
     

18.什么是负载因子?map的负载因子为什么是6.5?

  1. 负载因子是用于衡量当前哈希表空间占用率的核心指标,也就是每个bucket桶存储的平均元素个数.Go官方发现,装载因子越大,填入的元素越多.空间利用率越高.但是发生hash冲突的几率就会变大.反之装载因子越小,填入的数据就越少,冲突发生的几率就越小.但是空间浪费就会变的更严重.而且还会提高扩容操作的次数.根据这个测试结果和讨论,官方综合给出了一个较为适中的值.把Go中的map负载因子硬编码置为6.5,这就是map中负载因子是6.5的由来.这就意味着Go语言中当map存储的元素个数大于或者等于6.5*桶的个数的时候就会出发扩容行为.

19.map如何扩容

双倍扩容:数据太多,增加桶的数量.扩容采取了一种称为渐进式的方式,原有的key并不会一次性搬迁完成,每次最多只会搬迁2个bucket.

等量扩容:重新排列,(是一个整理的过程.)极端情况下,重新排列也解决不了,map存储就会蜕变成链表,性能大大降低,此时哈希因子hash0的设置,可以降低此类极端情况发生.

总结:(引用Go语言map的扩容)

  • 装载系数或者溢出桶的增加,会触发map扩容
  • “扩容”可能并不是增加桶的数量,而是整理数据,使数据更加紧凑
  • map扩容采用渐进式,桶被操作时才会重新分配

20.map和sync.Map谁的性能最好,为什么?

先说结论,(引用golang sync.Map和map+RWmutex性能比较)

  1. sync.map的性能高体现在读多写少的情况下,极端情况下,只有读操作的时候性能要远高于普通map.性能可以达到44倍左右.反过来,如果是全写,没有读操作,那么sync.map的性能只有map+mutex性能的一半.建议使用sync.map的时候需要考虑读写比例.当写操作之战读操作<=1/10的时候使用sync.map性能会明显提高.
    这是sync.map底层的数据结构.
    
    type Map struct {
     mu Mutex
     read atomic.Value // readOnly缓存字段.读取map的时候不加锁.
    //写入的时候要顺带更新这个字段的缓存.空间占用率会更高.
    //使用空间换读取操作的时间.
     dirty map[interface{}]*entry  //写入时候需要写入的真实map.操作.
     misses int
    }
     
    // Map.read 属性实际存储的是 readOnly。
    type readOnly struct {
     m       map[interface{}]*entry  
     amended bool
    }

  2. 因为sync.map底层维护了一个read的原子性结构体字段.这个字段在进行读取的时候是不需要加锁的.所以在所以sync.map在读场景的话会非常快.但是在写场景的话sync.map不仅要更新map的数据.还要更新read字段的缓存数据.所以每次更新都是两个更新操作.所以在写场景下sync.map效率更低.
  3. 在原生map的场景下.并发操作读的时候需要加R锁(读锁),为了方式在这个时候有其他的并发操作对该map进行修改.导致数据读取不准确.而在写入的时候只要单次写入map数据即可.不需要维护sync.map才有的缓存字段.所以只有一次性的写入操作.
  4. 综上所述在读多写少的场景(比例为:10:1的或者更多的情况下,)使用sync.map更为合适.如果写多读少的情况使用map+RWmutex更合适.

21.channel有什么特点

  1. 给一个nil channel发送数据会导致永远阻塞
  2. 从一个nil channel中接收数据.会造成永远阻塞.
  3. 给一个已经关闭的channel发送数据,会引起panic
  4. 从一个已经关闭的channel接收数据,如果缓冲区为空,则返回一个零值
  5. 无缓冲区的channel是同步的.有缓冲区的channel是异步的.

22.channel的底层实现原理是什么?

  1. Go中的channel是一个队列,遵循先进先出的原则.负责协程之间的通讯,(Go语言提倡不要通过共享内存来实现通信,而是要通过通信来实现内存的共享,CSP并发模型就是通过goroutine和channel来实现的.)通过var声明或者make函数创建的channel变量是一个存储在函数栈上的指针.占用8个字节,指向堆上的hchan结构体.
type hchan struct {
 closed   uint32   // channel是否关闭的标志
 elemtype *_type   // channel中的元素类型
 // channel分为无缓冲和有缓冲两种。
 // 对于有缓冲的channel存储数据,使用了 ring buffer(环形缓冲区) 来缓存写入的数据,本质是循环数组
 // 为啥是循环数组?普通数组不行吗,普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部都前移
 // 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
 buf      unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)
 qcount   uint           // 循环数组中的元素数量
 dataqsiz uint           // 循环数组的长度
 elemsize uint16                 // 元素的大小
 sendx    uint           // 下一次写下标的位置
 recvx    uint           // 下一次读下标的位置
 // 尝试读取channel或向channel写入数据而被阻塞的goroutine
 recvq    waitq  // 读等待队列
 sendq    waitq  // 写等待队列
 lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}

等待队列:双向链表,包含一个头结点和一个尾结点

每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里

type waitq struct {
   first *sudog
   last  *sudog
}
type sudog struct {
    g *g
    next *sudog
    prev *sudog
    elem unsafe.Pointer 
    c        *hchan 
    ...
}

总结hchan结构体的主要组成部分有四个:

  • 用来保存goroutine之间传递数据的循环数组:buf
  • 用来记录此循环数组当前发送或接收数据的下标值:sendx和recvx
  • 用于保存向该chan发送和从该chan接收数据被阻塞的goroutine队列: sendq 和 recvq
  • 保证channel写入和读取数据时线程安全的锁:lock
  • 引用(channel原理)
  • 引用2(channel实现原理以及实例详解.)

23.channelyou无缓冲的区别

  1. 不带缓冲的channel是同步的,带缓冲的channel是异步的,不带缓冲的channel中每一个发送者与接受者都会祖泽当前现场,只有当接受者与发送者都准备就绪了,channel才能正常使用.带缓冲的channel并不能无线接受数据而不造成阻塞,能够接受的个数取决于channel定义是,设定的缓冲的大小.只有在这个缓冲范围内,向channel发送的数据才不会造成channel阻塞.

24.channel为什么是线程安全的?

  1. 不同协程之间的通信本身的使用场景就是多线程的,为了保证数据一致性,必须实现线程安全.因此channel的底层实现中,hchan的结构体中采用了Mutex锁来保证数据读写安全.在对循环数组buf中的数据进行入队和出队操作是,必须先获取到互斥锁才可以操作channel中的数据.

25.channel如何控制goroutine并发执行程序

  1. 使用channel进行通信通知.用channel去传递信息.从而控制并发执行顺序.
  2. 使用channel的通信机制进行goroutine执行顺序的控制.
  3. 首先是默认情况下读goroutine在没接收到channel中信号的时候是处于阻塞状态.
  4. 那么可以利用这个阻塞状态对需要优先执行的goroutine A进行先执行,然后A执行过程结束后将B需要的channel写入到B  channel中.
  5. 此时正在阻塞的B goroutine收到 Bchannel发送的一个收信号,然后执行B goroutine操作.同理.在B goroutine执行完成之后,将C需要的执行信号发送到C对应的channel中.
  6. 这样C就可以从channel中获取执行信号.来执行C的操作.那么最终结果就是虽然是在主函数中是同时开启的goroutine操作.但是.因为channel和gorountine的通信机制导致需要串行执行.

channe共享内存有什么优劣势

  •  Go引入了channel和Goroutine实现CSp模型将生产者消费者进行了解耦,Channel其实和消息队列很相似.
  1. 优点:使用channel可以帮助我们解耦生产者消费者,可以降低并发中的耦合.
  2. (高内聚低耦合:一个完整的系统,模块与模块之间,尽可能的使其独立存在,也就是说让每个模块尽可能的独立完成某个特定的子功能,模块与模块之间的接口尽量的少而简单.
  3. 代码内聚就是一个模块内各个元素彼此结合的紧密程度,高内聚就是一个模块内各个元素彼此结合的紧密程度高,所谓高内聚市值一个软件模块是有相关性很强的代码租车.只负责一项任务.也就是常说的单一职责原则.
  4. 高内聚低耦合的好处:短期看没有很明显额好处.甚至短期内会影响系统的开发进度.因为高内聚低耦合的系统对开发设计人员提出来更高的要求.长期看低耦合的模块便于进行单元测试,.且易于维护.)
  • 缺点:容易死锁.

26.如何那种情况下channel会出现死锁现象?应该怎么解决?或者预防死锁出现?


死锁:

单个协程永久阻塞
两个或两个以上的协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象。
channel死锁场景:

非缓存channel只写不读
非缓存channel读在写后面
缓存channel写入超过缓冲区数量
空读
多个协程互相等待
总结:空读满写

  • 举例.当一个channel中没有数据而直接读取的时候,会发生死锁.
q := make(chan int,2)
<-q

解决方案是采用select语句,再加上default默认处理方式:


q := make(chan int,2)
select{
   case val:=<-q:
   default:
         ...
 }

当channel中写满后再进行数据写入的时候会造成死锁(有缓冲去的情况下)

q := make(chan int,2)
q<-1
q<-2
q<-3

无缓冲区的情况下,只有写操作没有读操作,只要向channel写入数据就会造成死锁.

q := make(chan int)
q<-1

解决方法:防止过度写入,添加select方法.让过渡写入的数据走select中的default选项.

func main() {
	q := make(chan int, 2)
	q <- 1
	q <- 2
	select {
	case q <- 3:
		fmt.Println("ok")
	default:
		fmt.Println("wrong")
	}

}

注意:向已经关闭的channel中再次写入数据,此时造成的错误不是死锁.而是panic.解决方法只有不像channel中写入数据.但是可以从已经关闭的channel中读取数据.

上述提到的死锁,是指在程序的主线程中发生的情况,如果上述的情况发生在非主线程中,读取或者写入的情况是发生堵塞的,而不是死锁。实际上,阻塞情况省去了我们加锁的步骤,反而是更加有利于代码编写,要合理的利用阻塞。。

27.Go互斥锁的实现原理

引用GO 互斥锁实现原理剖析

type Mutex struct {
	state int32
	sema  uint32
}

Mutex.state表示互斥锁状态.比如是否被锁定.

Mutex.sema表示心好累.协程阻塞等心好累.解锁的协程释放信号量从而环形等待信号的协程.

state内部实现时把该变量分成了四份.并用来记录Mutex的四种状态.

在这里插入图片描述

  1.  Locked: 表示该Mutex是否已经被锁定.0:表示没有被锁定. 1: 表示已经被所动.
  2. worked: 表示是否有协程已被唤醒,0:表示没有,1:表示有协程唤醒,正在加锁过程中.
  3. starving: 表示该Mutex是否处于饥饿状态,0表示没有饥饿,1表示饥饿.说明有协程阻塞超过了1ms.

Waiter: 表示阻塞等待锁的协程个数.协程解锁时会根据这个值来判断是否需要释放信号量.

协程之间枪锁实际上是抢着给locked赋值的权利.能给locked置为1,说明枪锁成功.抢不到的话就要阻塞等待mutex.sema信号量.一旦持有锁的协程解锁.等待的协程会依次被唤醒.

worked和starving主要用于控制协程之间的枪锁过程.

28.什么是自旋?

自旋对应CPU的"PAUSE"指令,CPU对应该指令什么都不做.相当于CPU空转,对程序来说相当于sleep了一小段时间.时间非常短.当前实现是30个CPU时钟周期.(主频不一样周期也不一样.)

加锁时程序会自动判断是否可以自旋,无限制的自旋将会给CPU带来巨大的压力.所以判断是否可以自旋很重要.

自旋必须满足以下所有条件:

  1. 锁已被占用,并且锁不处于饥饿模式。
  2. 积累的自旋次数小于最大自旋次数(active_spin=4)。
  3. cpu 核数大于 1。
  4. 有空闲的 P。
  5. 当前 goroutine 所挂载的 P 下,本地待运行队列为空。

自旋的优势是充分利用cpu,尽量避免协程切换,因为当前申请加锁的协程拥有cpu,如果经过短时间的自旋可以获得锁,那么协程可以继续执行.不比进入阻塞状态和切换cpu线程.(自旋是指正在执行的程序自己为了获取更多的执行时间而产生的.如果短时间内无法执行完成该程序,那么就会自己再次获取锁权限,接着进行执行.如果执行时间超过4次自旋时间,那么就需要进行排队.)

29.互斥锁正常模式和饥饿模式的区别

正常模式(非公平锁): 

  1.         解释一: 所有等待锁的goroutine按照FiFO(先进先出顺序等待.),唤醒的goroutine不会直接拥有锁,而是会和心情求的Goroutine竞争锁,新请求的goroutine更容易获抢占锁,,因为他正在CPU上执行,会有自旋锁与心唤醒的goroutine抢占锁,在这种情况下新唤醒的goroutine会更大程度上抢占到锁.这种情况下新唤醒的groutine会被加入到等待队列的前面,等到锁被释放后,队列前面的goroutine才会再次被唤醒,进行优先抢占锁使用权.\
  2.         解释二: 正常模式下协程如果加锁不成功不会立即转入等待队列.而是判断是否满足自旋条件.如果满足自旋条件.那么当蚩尤蓑的协程释放锁的时候,会释放一个信号量来唤醒等待队列中的协程.如果有协程处在自旋过程中,锁往往会被该自旋锁获得.被唤醒的协程只能再次阻塞,不过阻塞钱会判断自上次阻塞到本次阻塞经过了多少时间.如果超过1msmutex将会进入饥饿模式.
  3. 上面的是查到的资料
  4. 下面是自己的总结.(自旋锁是一个新创建的goroutine在cpu上运行的时产生的尝试给自己加锁的一个称呼.)
正常模式下会有goroutine尝试自旋加锁.
(抢占模式.如果多次之后未抢到锁.
就到队列尾部排队.),如果1ms之内抢到了锁,那么就执行加锁.
然后其他的goroutine中有一部分就继续排队等待另一部分尝试自旋加锁..

当队列中的goroutine被阻塞后在队列中的时间超过1ms的时候
这个goroutine就处于饥饿状态.那么前面无论是否有自旋锁在
抢占加锁.都会将锁分配给队列中首个饥饿状态中的goroutine,

如果占有锁的goroutine占有市场地域1ms就释放锁了.那么就
不会出现饥饿模式,如果队列中没有goroutine在排队,那么也
不会出现饥饿模式.

30.Go 读写锁的实现原理  

引用(Go语言读写锁 RWMutex 详解)

读写锁RWMutex,是对Mutex的一个扩展,当一个Goroutine获得了读锁之后.其他goroutine

可以获取读锁,但是不能获取写锁.当一个Goroutine获取写锁后其他goroutine既不能获取读锁也不能获取写锁.(只能存在一个写或者 多个读.)

底层实现结构:

type RWMutex struct {
	w           Mutex  // 控制 writer 在 队列B 排队
	writerSem   uint32 // 写信号量,用于等待前面的 reader 完成读操作
	readerSem   uint32 // 读信号量,用于等待前面的 writer 完成写操作
	readerCount int32  // reader 的总数量,同时也指示是否有 writer 在队列A 中等待
	readerWait  int32  // 队列A 中 writer 前面 reader 的数量
}

// 允许最大的 reader 数量
const rwmutexMaxReaders = 1 << 30

举例:假设当前有两个 reader,readerCount = 2;允许最大的reader 数量为 10

  • 当 writer 进入队列A 时,readerCount = readerCount - rwmutexMaxReaders = -8,readerWait = readerCount = 2
  • 如果再来 3 个reader,readerCount = readerCount + 3 = -5
  • 获得读锁的两个reader 执行完后,readerCount = readerCount - 2 = -7,readerWait = readerWait-2 =0,writer 获得锁
  • writer 执行完后,readerCount = readerCount + rwmutexMaxReaders = 3,当前有 3个 reader
     

实现方法:

func (rw *RWMutex) RLock() // 加读锁
func (rw *RWMutex) RUnlock() // 释放读锁
func (rw *RWMutex) Lock() // 加写锁
func (rw *RWMutex) Unlock() // 释放写锁

RWMutex读写优先策略:

  1. 队列 A 最多只允许有 一个writer,如果有其他 writer,需要在 队列B 等待;
  2. 当一个 writer 到了 队列A 后,只允许它 之前的reader 执行读操作,新来的 reader 需要在 队列A 后面排队;
  3. 当前面的 reader 执行完读操作之后,writer 执行写操作;
  4. writer 执行完写操作后,让 后面的reader 执行读操作,再唤醒队列B 的一个 writer 到 队列A 后面排队。
     

互斥锁与读写锁的区别:

  1. 读写锁区分读操作和写操作,而互斥锁是不区分读写的.只要申请锁那么在锁释放前,其他的任何一种锁都是不会申请成功的.只能等待.
  2. 互斥锁同一时间只允许一个线程访问该对象,无论读写操作,读写锁同一时间只允许一个写操作,但是可以有多个读操作可以同时执行.

31.Go原子操作有哪些?

  • 原子操作仅会由一个独立的CPU指令代表和完成.原子操作是无锁的.尝尝直接通过CPU指令直接实现.其他同步技术的实现尝尝依赖原子操作.
  • 当我们想要对某个变量并发安全的操作的时候,除了可以使用官方提供的MuTex之外,还可以使用sync.atomic包的原子操作.能够保证变量在读取或者修改的时候不被其他协程锁影响.
  • atomic包提供的原子操作能够保证任意时刻都只有一个goroutine对变量进行操作.善用atomic能够避免程序中出现大量的锁操作.
  • 常见操作:
    • 增减Add
    • 载入Load
    • 比较并交换CompareAndSwap
    • 交换Swap
    • 存储Store

32.原子操作和锁的区别是什么?

  1. 原子操作是由底层硬件支持,而锁是基于原子操作信号+信号量完成的.若实现相同的功能,前者通常会更有效率,
  2. 原子操作是单个指令的互斥操作;互斥锁/读写锁都是一种数据结构,可以完成临界区(多个指令)的互斥操作.扩大原子操作的范围.
  3. 原子操作是无锁状态.属于乐观锁;一般使用的属于悲观锁.
  4. 原子操作存在与各个指令/语言层级,比如机器指令层级的原子操作.汇编层级的原子操作.Go语言级别的原子操作等.
  5. 锁也存在于各个指令/语言中.比如机器指令层级,汇编指令层级的锁."go语言层级的锁"

33.Goroutine的底层实现原理?

Goroutine可以理解成Go语言的协程(轻量级的线程),是Go支持高并发的基础,属于用户态的线程.由GoRuntime管理而不是操作系统.他由语言本身和框架层调度.Golang在语言层面实现了调度器,同时对网络,IO,进行了封装处理.屏蔽了操作系统层面的复杂细节,在语言层面提供统一的关键字支持.

GMP 调度模型

G=Goroutine 协程,P=Processor 处理器, M=Thread 线程

  • 全局队列(Global Queue):存放等待运行的 G。
  • P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
  • P:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去.

GMP对应关系

  1. Goroutine调度器和OS调度器是通过M结合起来的.每个M都会与1个内核线程进行绑定,OS调度器负责吧内核线程分配到CPU的核心上执行,在运行是一个M同时只能绑定一个P,M和P是1对1 绑定的.M和P的组合构成了G的有效运行环境.但是M和P会实时的组合和断开,以保证待执行的G队列能够得到及时的执行,而P和G的关系是1对多的,多个可执行G会顺序排查一个队列挂载某个P上,在运行过程中,M和内核线程之间的对应关系是不变的.在M的声明周期内他只会和一个内核线程绑定,二M和P以及P和G之间的关系都是动态可变的.
  2. M与O的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以即使P的数量是1 ,也可能会创建多个M出来.但是因为P的存在,G和M可以呈现多对多的关系,当一个正在与某个M对接并运行这都的G,需要因某个事件,比如等待IO或者等待锁的解除.而暂停运行的时候,调度器总会即使的发现,并吧这个G与那个M分离开,用来释放计算资源供那些等待运行的G使用.

M和P何时会被创建?

P何时会被创建?

  •         在确定了P的最大数量n后,运行时系统会根据这个数量来创建n个P,

M何时会被创建?

  •         没有足够的M来关联P并运行其中的可运行的G的时候(意思就是G的可执行数量比较多的时候,并且有足够的P,这个时候如果M不足的话就会创建M),
  • 比如当前可用的所有的M此时都阻塞住了,而P中还要很多就绪任务,就会去寻找空闲的M,而没有空闲的M就会去创建M.

34. go func调度流程是什么?

在这里插入图片描述

当Processor中的G产生系统调用/IO时,处理流程如下,假设此时运行线程为M0:

  1. Processor感知M0正在处理的协程G0处于系统调用的阻塞状态
  2. Processor将G0从自己的G队列中移除
  3. Processor重新申请新的M1,来继续执行G队列
    1. 如果有空闲的M,则直接复用空闲M
    2. 如果无空间的M,则新建一个线程M
  4. M0执行完G0的系统调用后,G0将存放在全局G队列中,等待某个Processor唤起
  5. 同时M0也进入空闲状态,等待其他P复用,或者被销毁
  • 所以,系统中M的个数通常会略多于P的个数,但同时执行的M个数和P数量一样

引用(Golang - GMP模型)

注:Processor,它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列。

在 Go 中,线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上

35.goroutine和线程的区别是什么?

goroutine 线程
内存占用创建一个Goroutine的栈内存消耗为几KB,实际运行过程中如果开辟的栈内存空间不够用,那么会自行扩容.创建一个栈内存消耗为几百KB-一两兆的空间
创建和销毁goroutine是由runtime负责管理的,创建和销毁都是用户级别,创建和销毁资源都是runtime包进行管理.不需要和操作系统进行直接交互,需要跟操作系统直接交互申请创建资源和归还资源.所以增加了系统资源的开销.相比goroutine来说更重一些.
切换goroutine实现高并发的主要原因是由于切换开销更小,这也是和线程之间最主要的区别.goroutine的调度是协同式的,不会直接与操作系统内核打交道.当goroutine进行切换时,只有少量的寄存器需要保存和恢复,更多的系统级的应用状态和信息不需要进行保存和恢复.所以切换速度更快.代价更小线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给他的时间片,就会被其他可执行的线程抢占,线程切换过程中需要保存和恢复所有计算器中的信息,包含的更多系统界别的应用,所以恢复的数据也会更多.时间更长.

引用(Go goroutine)

36.Goroutine泄露的场景有哪些?

泄露原因

  1. Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。

  2. Goroutine 内的业务逻辑进入死循环,资源一直无法释放。

  3. Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。

channel使用不当:

channel发送没有接收者,

func main() {
    for i := 0; i < 4; i++ {
        queryAll()
        fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
    }
}
 
func queryAll() int {
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func() { ch <- query() }()
     }
    return <-ch
}
 
func query() int {
    n := rand.Intn(100)
    time.Sleep(time.Duration(n) * time.Millisecond)
    return n
}

输出结果:
goroutines: 3
goroutines: 5
goroutines: 7
goroutines: 9

这里的原因是每次每次调用queryAll都会启动3个Goroutine,但是每次return只会接受一个ch中的值,也就是会有两个ch的值是没有被接受的.这样就会导致每次都会有2个goroutine会处于等待数据接受状态,从而导致goroutine只增不减,出现泄露的情况.

goroutine启动后内部channel只接受数据.不进行数据发送

func main() {
    defer func() {
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()
 
    ch := make(chan struct{})
    go func() {
        ch <- struct{}{}
    }()
    
    time.Sleep(time.Second)
}

输出结果:
goroutines:  2

这里初始化完成channel后向channel中写入了一个空struct,但是并没有消费者对这个channel进行消费.(正常情况下也写不进去.因为没有缓存,)所以

nil channel

func main() {
    defer func() {
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()
 
    var ch chan int
    go func() {
        <-ch
    }()
    
    time.Sleep(time.Second)
}

输出结果:
goroutines:  2

这里出现goroutine泄露的原因是,启动goroutine消费channel中的信息,但是chanel中没有写入信息.,导致goroutine阻塞住,从而这个goroutine就不会被释放,一直监听channel消息的到来,(但是这里的channel并没有初始化,并没有分配内存空间,是无法将消息放到channel中的.所以这个goroutine每次运行后都无法释放.从而导致goroutine泄露.)

goroutine中的请求等待时间过长并且没有超时时间

func main() {
    for {
        go func() {
            _, err := http.Get("https://www.xxx.com/")
            if err != nil {
                fmt.Printf("http.Get err: %v\n", err)
            }
            // do something...
    }()
 
    time.Sleep(time.Second * 1)
    fmt.Println("goroutines: ", runtime.NumGoroutine())
 }
}
输出结果
goroutines:  5
goroutines:  9
goroutines:  13
goroutines:  17
goroutines:  21
goroutines:  25
...

这个例子中,展示了一个Go语言中经典的事故场景,也就是一般我们会在应用程序中取调用第三方服务接口,但是第三方接口有时候会很慢,久久没有返回响应结果,恰好 Go语言中默认的http.client是没有设置超时时间的.因此就会导致一直阻塞.多次请求就会导致一直上涨,(这里使用了for循环模拟多次请求.)

所以我们在用goroutine进行第三方接口调用的时候,需要加上超时时间.超过这个时间的话就发送一个channel信号将当前goroutine停止.

互斥锁忘记解锁

func main() {
    total := 0
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("total: ", total)
        fmt.Println("goroutines: ", runtime.NumGoroutine())
 }()
 
    var mutex sync.Mutex
    for i := 0; i < 10; i++ {
        go func() {
            mutex.Lock()
            total += 1
        }()
    }
}
输出结果:
total:  1
goroutines:  10

因为goroutine中加锁.后并没有进行解锁.所以total只能被加锁的goroutine进行操作.导致goroutine创建后无法释放.释放方法就是同一个goroutine中解锁即可.

    var mutex sync.Mutex
    for i := 0; i < 10; i++ {
        go func() {
            mutex.Lock()
            defer mutex.Unlock()
            total += 1
    }()
    }

同步锁使用不同步

func handle(v int) {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < v; i++ {
        fmt.Println("脑子进煎鱼了")
        wg.Done()
    }
    wg.Wait()
}
 
func main() {
    defer func() {
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()
 
    go handle(3)
    time.Sleep(time.Second)
}

这里的结果是调用函数的时候预先添加过量的wg.add数量.导致创建的数量与结束的数量不匹配.所以wg.wait就会一直不清零,导致一直阻塞.

正常使用是每次wg.add(1),  代码结束之前 defer wg.done(-1), 来使用.这样的话创建的数量和结束的数量是成对存在的.就不会存在goroutine阻塞泄露的情况了.

排查方法

调用runtime.NumGoroutine方法来获取运行数量.进行前后比较,就可以知道有没有泄露了.

但是线上业务不适应这个命令.所以大多数生产,测试,环境使用pprof进行检查.

泄露场景:

  1. 如果输出的Goroutines数量在不断增加,说明存在泄露的情况.

37.如何查看正在执行的goroutine数量

package main

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {

    for i := 0; i < 100; i++ {
        go func() {
            select {}
        }()
    }

    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()

    select {}
}

终端执行就可以看到(也可以通过页面查看) 引用(go性能分析工具pprof)

go tool pprof -http=:1248 http://127.0.0.1:6060/debug/pprof/goroutine

38.如何控制并发额goroutine数量?

可以使用Wait.Group启动指定数量的goroutine,监听channel的通知.发送者推送信息到channel,信息处理完了关闭channel,等待goroutine依次退出.

var (
 // channel长度
 poolCount      = 5
 // 复用的goroutine数量
 goroutineCount = 10
)
 
func pool() {
 jobsChan := make(chan int, poolCount)
 
 // workers
 var wg sync.WaitGroup
 for i := 0; i < goroutineCount; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   for item := range jobsChan {
    // ...
    fmt.Println(item)
   }
  }()
 }
 
 // senders
 for i := 0; i < 1000; i++ {
  jobsChan <- i
 }
 
 // 关闭channel,上游的goroutine在读完channel的内容,就会通过wg的done退出
 close(jobsChan)
 wg.Wait()
}

39.GMP和GM模型

  1. G(Goroutine) : 代表Go协程Goroutine,存储了Goroutine的执行栈信息,Goroutine状态以及Goroutine的任务函数等,G的数量无限制,理论上只收内存大小影响.创建一个G的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个Goroutine,而且Go在G退出的时候会吧G清理之后放到P本地或者全局闲置列表,gFree中以便下次复用,
  2. M(Machin : Go对操作系统线程(OS Thread) 的封装,可以看做操作系统的内核线程,想要在CPU上执行代码必须有线程,通通过系统调用clone创建,M在绑定有效P之后,进入一个调度循环,二调度循环的机制大致是从P的本地运行队列以及全局队列中获取G,切换到G的执行栈上执行G的函数,调用goexit做清理工作并回到M上,如此反复,M并不保留G的状态,这是G 可以跨M调度的基础,M的数量是有限的,默认最大是10000,可以通过debug.SetMaxThreads()方法进行设置,如果他有空闲M那么就会回收或者睡眠,如果M不够用那么就会自动创建,M对应P的关系是(M:N),就是两者并不是绝对的数量绑定关系.
  3. P虚拟处理器,执行G所需要的资源和上下文,只有将P和M绑定,才能让P中的runq真正的运行起来,P的数量决定了系统内最大可并行的G的数量.  P的数量受本地CPU核心数量影响,可以通过改变环境变量 $GOMAXPROCS来设置CPU核心数.
  4. Sched 调度器结构,他维护有存储M和G的全局队列.以及调度器本身的状态信息.

40.Go调度原理(参考33,34条)

CPU是无法直接感知到Goroutine的,只知道内核线程,所以需要Go调度器将协程调度到内核线程上去,然后操作系统调度器将内核线程放到CPU上去执行.

M是对内核级线程的封装,所以Go调度器的工作就是将G分配到M上去,

41.Go hand  off机制

也称为P分离机制,当本线程 M 因为 G 进行的系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的 M 执行,也提高了线程利用率

 42.Go work stealing 机制

  • 获取P本地队列,当从绑定P的本地runq上找不到可执行的G,就会尝试从全局队列中找,如果全局队列中还是找不到,那就从netpool和事件池中拿,最后如果还是拿不到的话就从别的P队列中偷,P此时唤醒一个M,P继续执行其他程序,M寻找是否有空闲的P,如果有,则将该G对象移动到他本身,

  • P创建M.或者P找空闲的M来执行剩余的G.


  • 当一个 P 发现自己的 LRQ ()已经没有 G 时,会从其他 P “偷” 一些 G 来运行。看看这是什么精神!自己的工作做完了,为了全局的利益,主动为别人分担。这被称为 Work-stealing,Go 从 1.1 开始实现。
  • Go scheduler 使用 M:N 模型,在任一时刻,M 个 goroutines(G) 要分配到 N 个内核线程(M),这些 M 跑在个数最多为 GOMAXPROCS 的逻辑处理器(P)上。每个 M 必须依附于一个 P,每个 P 在同一时刻只能运行一个 M。如果 P 上的 M 阻塞了,那它就需要其他的 M 来运行 P 的 LRQ 里的 goroutines。

 引用(GoLang之什么是workstealing(5))

43.Go抢占式调度

真正的抢占式是基于信号完成的,所以也成为了"异步抢占",不管协程有没有意愿让出CPU执行权限,只要某个协程执行时间过长,就会发送信号强行夺取cpu使用权限.

  • M注册一个sigurg信号的处理函数,sighandler
  • sysmon启动后会间隔性的进行监控.最长间隔10ms,最短间隔20us,如果发现某协程独占P的时间超过10ms,会给M发送抢占信号.
  • M收到抢占信号后,内核执行sighandeler函数把当前协程的状态从_Gorunning正在执行改成Grunnable可执行,把抢占的协程放到全队队列里.M继续寻找其他Goroutine来执行.
  • 被抢占的G再次调度过来执行时,会继续原来的执行流.

翻译一下就是,goroutine每10ms就会切换一次,这个切换执行的goroutine要么是处在饥饿状态下的goroutine,要么是处在自旋锁状态的goroutine,要么就是P本地队列或者全局队列中的goroutine,


goroutine执行的优先级别是,饥饿>自旋锁状态goroutine>本地队列>全局队列

44.Go如何查看运行时调度信息

有两种方式可以查看一个程序的调度GMP信息,分别是go tool trance 和GODEBUG.

45.Go内存逃逸机制


概念
        在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在堆上分配,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸。

        在栈上分配的地址,一般由系统申请和释放,不会有额外性能的开销,比如函数的入参、局部变量、返回值等。在堆上分配的内存,如果要回收掉,需要进行 GC,那么GC 一定会带来额外的性能开销。编程语言不断优化GC算法,主要目的都是为了减少 GC带来的额外性能开销,变量一旦逃逸会导致性能开销变大。
逃逸机制

  •         编译器会根据变量是否被外部引用来决定是否逃逸:
  •         如果函数外部没有引用,则优先放到栈中;
  •         如果函数外部存在引用,则必定放到堆中;
  •         如果栈上放不下,则必定放到堆上;

总结

栈上分配内存比在堆中分配内存效率更高
栈上分配的内存不需要 GC 处理,而堆需要
逃逸分析目的是决定内分配地址是栈还是堆
逃逸分析在编译阶段完成
因为无论变量的大小,只要是指针变量都会在堆上分配,所以对于小变量我们还是使用传值效率(而不是传指针)更高一点。

46.内存对齐机制

因为不同类型的变量占用内存的大小是不一样的,但是cpu每次读取的内存长度是固定的,为了cpu能高效的读写数据(cpu读取数据不是一个字节一个字节读取的,一次读取的一块内存),所以编译器在编译的时候会通过填充空数据(数据不是连续的),让一个变量,使cpu能一次操作就能完成读写。还有跨平台的问题,有的平台不支持访问任意地址上的任意数据,必须按照顺序依次按块读取。

引用(聊一聊go的内存对齐)
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7498.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GICv3和GICv4虚拟化

本文档翻译自文档Arm Generic Interrupt Controller v3 and v4 - Virtualization 1 虚拟化 Armv8-A选择性的支持虚拟化。为了完成该功能&#xff0c;GICv3也支持虚拟化。GICv3中对虚拟化的支持包括如下功能&#xff1a; CPU Interface寄存器的硬件虚拟化产生和发送虚拟中断的…

pytorch深度学习实战lesson14

第十四课 丢弃法&#xff08;Dropout&#xff09; 目录 理论部分 实践部分 从零开始实现&#xff1a; 简洁实现&#xff1a; 理论部分 这节课很重要&#xff0c;因为沐神说这个丢弃法比上节课的权重衰退效果更好&#xff01; 为什么期望没变&#xff1f; 如上图所示&#…

java main方法控制日志级别

背景&#xff1a; 今天想用main方法去调用http请求&#xff0c;结果已经没什么问题了&#xff0c;但是打印了一大堆Http业务内部的日志信息&#xff0c;特别挡路&#xff0c;导致想看到的业务输出看不到&#xff0c;所以经过多方求证&#xff0c;进行了日志等级处理。 默认情…

【Pytorch with fastai】第 5 章 :图像分类

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

电商项目缓存问题的解决方案(初步)

内容分类 容量规化 架构设计 数据库设计 缓存设计 框架选型 数据迁移方案 性能压测 监控报警 领域模型 回滚方案 高并发 分库分表 优化策略 负载均衡 软件负载 nginx&#xff1a;它自身的高可用是用lvs去保证。 下单需要登录 > 需要Session > 分布式Ses…

好书赠送丨海伦·尼森鲍姆著:《场景中的隐私——技术、政治和社会生活中的和谐》,王苑等译

开放隐私计算 收录于合集#书籍分享1个 开放隐私计算 开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神&#xff0c;专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播&#xff0c;愿成为中国 “隐私计算最后一公里的服…

[附源码]java毕业设计基于javaweb电影购票系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

公众号运营建议与反思分享,建议收藏

正所谓有总结才会有成长&#xff0c;公众号运营也是如此。 公众号运营不是一朝一夕的事情&#xff0c;经过岁月的洗礼和千锤百炼&#xff0c;也总归是有了自己的一套经验和技巧。 对于公众号运营有什么建议&#xff1f;值得大家反思什么&#xff1f;今天伯乐网络传媒就来给大…

Boost升压电路调试

背景&#xff1a; 项目用到了一款升压电路&#xff0c;将12V升压到32V&#xff0c;电流要求有12A&#xff0c;最大18A。 设计的方案是使用Boost Controller 外置MOS来实现。 选定的Controller芯片为Maxim的MAX25203。 问题&#xff1a; 回板后进行调试&#xff0c;在不使能…

活动预告|“构建新安全格局”专家研讨会即将开幕

应急管理承担着防范化解重大风险、及时应对处置各类突发事件的重要职责&#xff0c;担负保护人民群众生命财产安全和维护社会稳定的重要使命。过去一年是我国应急管理体系和能力建设经受严峻考验的一年&#xff0c;也是实现大发展的一年。 11月17日&#xff0c;由中央党校科研部…

Python简单实现人脸识别检测, 对某平台美女主播照片进行评分排名

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 开发环境: Python 3.8 Pycharm 2021.2 模块使用: 第三方模块 requests >>> pip install requests tqdm >>> pip install tqdm 简单实现进度条效果 自带模块 os base64 采集代码 导入模块 # 数…

vue封装的echarts组件被同一个页面多次引用无法正常显示问题(已解决)

问题&#xff1a;第二张图显示空白&#xff0c;折线图并没有展示出来 当我们在封装了echarts组件之后&#xff0c;需要在同一个页面中引入多次时&#xff0c;会出现数据覆盖等一系列问题 当时我是修改了id也无济于事&#xff0c;达不到我需要的效果 解决方案 将我们封装的组件…

HTML5简明教程系列之HTML5 表格与表单(二)

HTML的第二弹也来了&#xff0c;最近高产似母猪&#xff0c;状态也不错&#xff0c;代码来源为实验课。本期主要内容为&#xff1a;HTML表格与DIV应用、HTML表单。上期基础部分的传送门&#xff1a; HTML5简明教程系列之HTML5基础&#xff08;一&#xff09;_Thomas_Lbw的博客-…

【进程复制】

目录地址偏移量fork函数fork练习地址偏移量 PCB结构体&#xff1a; struct task_struct { PID ststus ; … } 页面的内存大小是固定的&#xff0c;不足一页会给一页&#xff0c;大于一页会给一个整页数 比如一页大小为4K&#xff0c;地址除4K商是页号&#xff0c;余数是在该页…

Vue(六)——使用脚手架(3)

目录 webStorage localStorage sessionStorage todolist案例中使用 组件自定义事件 绑定 解绑 总结 全局事件总线 消息发布与订阅 nextTick 过渡与动画 webStorage 这不是vue团队开发的&#xff0c;不需要写在xx.vue当中&#xff0c;只需写在xx.html当中即可。 什…

Linux下C++开发笔记--g++命令

目录 1--前言 2--开发环境搭建 3--g重要编译参数 4--实例 1--前言 最近学习在linux环境下进行C开发的基础知识&#xff0c;参考的教程是基于VSCode和CMake实现C/C开发 | Linux篇&#xff0c;非常适合小白入门学习。 2--开发环境搭建 ①安装gcc、g和gdb&#xff1a; sud…

深度学习入门(三十七)计算性能——硬件(TBC)

深度学习入门&#xff08;三十七&#xff09;计算性能——硬件&#xff08;CPU、GPU&#xff09;前言计算性能——硬件&#xff08;CPU、GPU&#xff09;课件电脑提升CPU利用率①提升CPU利用率②CPU VS GPU提升GPU利用率CPU/GPU带宽更多的CPU和GPUCPU/GPU高性能计算编程总结教材…

SpringBoot整合dubbo(一)

第一次整合&#xff0c;使用无注册中心方式 一、首先&#xff0c;项目分为三个模块&#xff0c;如下图&#xff0c;dubbo-interface&#xff08;要发布的接口&#xff09;、dubbo-provider&#xff08;接口的具体实现&#xff0c;服务提供者&#xff09;、dubbo-consumer&#…

【LeetCode-中等】63. 不同路径 II(详解)

题目 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 现在考虑网格中有障碍物。那么从左上角到…

VScode

VScode 下载 VScode&#xff1a;https://code.visualstudio.com/安装 汉化 Chinese (Simplified) 设置 背景色 Atom One Light Theme Color Theme 护眼色 "workbench.colorCustomizations": { // 设置背景颜色// "foreground": "#75a478",&…