Golang Sync.Mutex 详解
lock逻辑:
- 第一次上锁的时候,直接走第一步CAS上锁,成功返回
- Mutex已经被另一个g上锁,那么state的g等待数+1,更新当前的锁状态,然后就进入队列,等待被唤醒,等到另个g调用了Unlock方法之后,当前g被唤醒,然后设置awoken=true,再执行一遍for循环,此时locked位就是未上锁状态(0),new就是代表上锁,然后清除woken位,然后再CAS更新new到state上,因为之前的锁是未上锁状态,那么就代表抢锁成功,break,返回
- 和第二种一样,只不过,在CAS更新new到state上时,有其他g先改掉了state的值,那么就继续for循环,然后重复到第二种情况。
自旋锁:
简单概括一下,就是为了解决锁粒度非常小的时候,给系统带来的不必要的调度开销
不过自旋要先满足几个条件
首先程序要跑在多核的机器上,然后GOMAXPROCS要大于1,并且此时有至少一个P的local runq是空的,才能进入到自旋的状态
自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序,所以 Goroutine 进入自旋的条件非常苛刻
当Mutex已经上锁的时候,当前G在满足自旋条件下,进入自旋状态,在自旋中,其他G解锁了Mutex,那么当前G就设置了woken标记位,这样其他G在Unlock的时候就不会去等待队列里面唤醒G了,然后当前G就顺理成章的抢到了锁
这样自旋锁在锁粒度非常小的场景下的能对其性能带来一定的优化。
引入自旋锁之后,又带来了一个问题。就是G等待队列的长尾问题。因为从等待队列里面被唤醒,然后再去抢锁,对本身就在执行的G来说,被唤醒的G其实是很难抢过当前执行的G的,这样的话,等待队列里面的G,就会被饿死(长时间获取不到锁),这样对等待队列的G来说其实是不公平的。
饥饿模式
简单概括一下,就是解决了等待G队列的长尾问题
饥饿模式下,直接由unlock把锁交给等待队列中排在第一位的G,同时,饥饿模式下,新进来的G不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部。
饥饿模式的触发条件,当一个G等待锁时间超过1毫秒时,Mutex切换到饥饿模式
饥饿模式的取消条件,当一个G获取到锁且在等待队列的末尾,或者这个G获取锁的等待时间在1ms内,那么Mutex切换回正常模式
带来的改变
Mutex.state的倒数第三位,变成了mutexStarving标记位,0表示正常模式,1表示饥饿模式,与此同时,支持的最大等待G数量从230个 变成了229个