🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] ### **Go 如何实现原子操作?** 原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行 的过程中,CPU 绝不会再去进行其他的针对该值的操作。 Go 语言的标准库代码包 sync/atomic 提供了原子的读取(Load 为前缀的函数)或写入(Store 为前缀的函数)某个值 ### **原子操作与互斥锁的区别** 1)、互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。 2)、原子操作是针对某个值的单个互斥操作。 ### **Mutex 几种状态** ![](https://img.kancloud.cn/90/a1/90a19a9aeb2c5b940d7f75532e163288_667x368.png) * mutexLocked — 表示互斥锁的锁定状态; * mutexWoken — 表示是否有协程被唤醒; * mutexStarving — 当前的互斥锁进入饥饿状态; * waitersCount — 当前互斥锁上等待的 Goroutine 个数; ### **饥饿模式和正常模式** #### **正常模式(非公平锁)** 正常模式下waiter 都是进入先入先出队列,被唤醒的 waiter 并不会直接持有锁,而是要和新来的 goroutine 进行竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以,在高并发情况下,被唤醒的 waiter 可能比较悲剧地获取不到锁,这时,它会被插入到队列的前面。 ##### 正常锁-->饥饿锁触发条件: > 如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么,这个 Mutex 就进入到了饥饿模式。 #### **饥饿模式(公平锁)** Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin,它会乖乖地加入到等待队列的尾部。 ##### 饥饿-->正常 转换: > * 此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了; > * 此 waiter 的等待时间小于 1 毫秒。 ### **WaitGroup 用法** 一个 WaitGroup 对象可以等待一组协程结束。使用方法是: 1. main 协程通过调用 wg.Add(delta int) 设置 worker 协程的个数,然后创 建 worker 协程; 2.worker 协程执行结束以后,都要调用 wg.Done(); 3.main 协程调用 wg.Wait() 且被 block,直到所有 worker 协程全部执行结束 后返回。 ### **WaitGroup 实现原理** 1、WaitGroup 主要维护了 2 个计数器,一个是请求计数器 v,一个是等待计数 器 w,二者组成一个 64bit 的值,请求计数器占高 32bit,等待计数器占低 32bit。 2、每次 Add 执行,请求计数器 v 加 1,Done 方法执行,等待计数器减 1,v 为 0 时通过信号量唤醒 Wait()。 ### **什么是 sync.Once** 1. Once 可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场 景。 2. Once 常常用来初始化单例资源,或者并发访问只需初始化一次的共享资 源,或者在测试的时候初始化一次测试资源。 3.sync.Once 只暴露了一个方法 Do,你可以多次调用 Do 方法,但是只有第 一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值 的函数。 ### **goroutine 的自旋占用资源如何解决?** 自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,直到获取到锁才会退出循环。 > 自旋条件 1. 锁已被占用,并且锁不处于饥饿模式。 2. 积累的自旋次数小于最大自旋次数(active\_spin=4)。 3. CPU 核数大于 1。 4. 有空闲的 P。 5. 当前 Goroutine 所挂载的 P 下,本地待运行队列为空。 mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里。 ### **sync.Pool 有什么用** 对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。频 繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛 刺。而 sync.Pool 可以将暂时将不用的对象缓存起来,待下次需要的时候直 接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系 统的性能。 ### **RWMutex 实现** 通过记录 readerCount 读锁的数量来进行控制,当有一个写锁的时候,会将读 锁数量设置为负数 1<<30。目的是让新进入的读锁等待之前的写锁释放通知读 锁。同样的当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才会开始 21 进行后续的操作。 而等写锁释放完之后,会将值重新加上 1<<30, 并通知刚才 新进入的读锁(rw.readerSem),两者互相限制。 ### **RWMutex 注意事项** 1. RWMutex 是单写多读锁,该锁可以加多个读锁或者一个写锁 2. 读锁占用的情况下会阻止写,不会阻止读,多个 Goroutine 可以同时获取 读锁 3. 写锁会阻止其他 Goroutine(无论读和写)进来,整个锁由该 Goroutine 独占 4. 适用于读多写少的场景 5. RWMutex 类型变量的零值是一个未锁定状态的互斥锁 6. RWMutex 在首次被使用之后就不能再被拷贝 7. RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic 8. RWMutex 的一个写锁去锁定临界区的共享资源,如果临界区的共享资源已 被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到解锁 9. RWMutex 的读锁不要用于递归调用,比较容易产生死锁 10. RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可 以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock) 11. 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并 都可以成功锁定读锁 12. 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而 被阻塞的 Goroutine,其中等待时间最长的一个 Goroutine 会被唤醒