[Go 并发编程实战课]02.Mutex 源代码
CAS 是实现互斥锁和同步原语的基础:
CAS 指令将给定的值和一个内存地址中的值进行比较,如果是同一个值,就使用新值替换内存地址中的值。CAS 是原子性的操作,保证这个指令总是基于最新的值进行计算,如果同时有其他线程已经修改了这个值,那么,CAS会返回失败。
学习 mutex 源代码,鸟窝的:sync.mutex 源代码分析
源代码分析
首先学习下 Mutex 注释,这些注释将极大的帮助我们理解Mutex
的实现。
sync.Mutex
包含两个字段
state
是一个共用字段。
第0个 bit 标记这个 mutex 是否已被某个 goroutine 所拥有,为0表示未被锁。
第1个 bit 标记这个 mutex 是否已唤醒,也就是有某个唤醒的 goroutine 要尝试获取锁。
第2个 bit 标记这个 mutex 状态,值 1 表示此锁已处于饥饿状态。
同时,尝试获取锁的 goroutine 也有状态,有可能它是新来的 goroutine,也有可能是被唤醒的 goroutine,可能是出于正常状态的 goroutine,也有可能是出于饥饿状态的 goroutine。
Lock
Unlock
饥饿模式和正常模式
请求锁时调用的 Lock 方法中一开始是 fast path,这是一个幸运的场景,当前的 goroutine 幸运的获得了锁,没有竞争,直接返回,否则就进入了 lockSlow 方法。这样的设计,方便编译器对 Lock 方法进行内联,可以在程序开发中应用这个技巧。
正常模式下,waiter 都是进入先入先出队列,被唤醒的 waiter 并不会直接持有锁,而是要和新来的 goroutine 进行竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以在高并发的情况下,被唤醒的 waiter 可能比较悲惨的获取不到锁,这时,它会被插入到队列的前面。
如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么这个 Mutex 就进入饥饿模式。
在饥饿模式下,Mutex 的拥有着将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会抢,也不会 spin,它会乖乖地加入到等待队列的尾部。
如果拥有 Mutex 的 waiter 发现下面两种情况之一,它就会把这个 Mutex 转换成正常模式:
此 waiter 已经是队列中的最后一个 waiter 了,没有其他等待锁的 goroutine。
此 waiter 的等待时间小于 1 毫秒。
正常模式拥有更好的性能,因为即使有等待抢锁的 waiter,goroutine 也可以连续多次获取到锁。
饥饿模式是对公平性和性能的一种平衡,它避免了某些 goroutine 长时间的等待锁。在饥饿模式下,优先对待的是那些一直在等待的 waiter。
逐步分析 Mutex 的关键代码
从请求锁(lockSlow)的逻辑看起。
首先要搞清楚 state 的意义,state 包含四个部分,mutexLocked 持有锁标记、 mutexWoken 唤醒标记、mutexStarving 饥饿标记、 mutexWaiterShift 阻塞等待的 waiter 数量。
第 16 行
var waitStartTime int64
记录 goroutine 请求锁的初始时间,第 17 行starving := false
标记是否处于饥饿状态,第 18 行awoke := false
标记是否唤醒,第 19 行iter := 0
记录 spin 的次数。第 22 行到第 39 行,它会对正常状态抢夺锁的 goroutine 尝试 spin,就是在临界区耗时很短的情况下提高性能。
第 41 行到第 56 行,非饥饿状态下抢锁,就是要把 state 的锁的那一位,置为加锁状态,后续 CAS 如果成功就可能获取到了锁。
第 57 行到第 60 行,如果锁已经被持有或者锁处于饥饿状态,等待,所以 waiter 的数量加 1。
第 61 行到第 69 行,如果此 goroutine 已经处在饥饿状态,并且锁还被持有,那么,我们需要把此 Mutex 设置为饥饿状态。
第 70 行到第 79 行,是清除 mutexWoken 标记,因为不管是获得了锁还是进入休眠,我们都需要清除 mutexWoken 标记。
第 82 行就是尝试使用 CAS 设置 state。如果成功,第 85 行到第 87 行是检查原来的锁的状态是未加锁状态,并且也不是饥饿状态的话就成功获取了锁,返回。
第 92 行判断是否第一次加入到 waiter 队列。到这里,你应该就能明白第 16 行为什么不对 waitStartTime 进行初始化了,我们需要利用它在这里进行条件判断。
第 99 行将此 waiter 加入到队列,如果是首次,加入到队尾,先进先出。如果不是首次,那么加入到队首,这样等待最久的 goroutine 优先能够获取到锁。此 goroutine 会进行休眠。
第 102 行判断此 goroutine 是否处于饥饿状态。注意,执行这一句的时候,它已经被唤醒了。
第 107 行到第 133 行是对锁处于饥饿状态下的一些处理。
第 118 行设置一个标志,这个标志稍后会用来加锁,而且还会将 waiter 数减 1。
第 128 行,设置标志,在没有其它的 waiter 或者此 goroutine 等待还没超过 1 毫秒,则会将 Mutex 转为正常状态。
第 131 行则是将这个标识应用到 state 字段上。
继续看下释放锁(Unlock)调用的 unlockSlow 方法:
如果 Mutex 处于正常状态,第 63 行直接唤醒等待队列中的 waiter。
如果 Mutex 处于饥饿状态,如果没有 waiter,或者已经有在处理的情况了,那么释放就好,不做额外的处理(第 40 行到第 42 行)。
否则,waiter 数减 1,mutexWoken 标志设置上,通过 CAS 更新 state 的值(第 45 行到第 50 行)。
思考
目前 Mutex 的 state 字段有几个意义,这几个意义分别是由哪些字段表示的?
等待一个 Mutex 的 goroutine 数最大是多少?是否能满足现实的需求?
评论