初入门径
// A Mutex is a **mut**ual **ex**clusion lock.
Mutex是互斥锁
// The zero value for a Mutex is an unlocked mutex.
互斥锁的零值是未锁定的互斥锁
// A Mutex must not be copied after first use.
首次使用后不得复制Mutex
mutual 英[ˈmjuːtʃuəl]
美[ˈmjuːtʃuəl]
adj. 相互的; 彼此的; 共有的; 共同的;
[例句]The East and the West can work together for their mutual benefit and progress
东西方可以为彼此共同的利益和发展而合作。
exclusion 英[ɪkˈskluːʒn]
美[ɪkˈskluːʒn]
n. 排斥; 排除在外; 不包括在内的人(或事物); 被排除在外的人(或事物); 排除; 认为不可能;
[例句]It calls for the exclusion of all commercial lending institutions from the college loan program
它提倡将所有商贷机构排除在高校贷款计划之外。
源码实现
sync.Mutex只是暴露给用户的前台提线木偶,真正的操控者,隐藏在runtime中的sema.go里
sync/mutex.go
1 | // A Mutex is a mutual exclusion lock. |
其中 state字段是一个int32,4字节 32bit。
最低一个bit的0和1,代表是否被锁。倒数第二位这个bit代表有没有被唤醒。倒数第三位这个bit代表是不是处于饥饿模式
剩下的29个bit位,代表等待锁的协程的数量
sema表面看是一个uint32,将其当成信号量锁来用的话,其实是一个SemaRoot的结构体,其中一个字段是treap,是一个指针,指向堆树(或称平衡二叉树),用于协程的排队(获取不到sema锁的话,协程会休眠被记录在treap中)
在Go项目的runtime包中的sema.go文件中,treap用于实现一个基于Treap数据结构的锁和信号量(semaphore)管理器。Treap是一种二叉搜索树和最小堆的组合结构,它将节点按照键值进行排序,并使用随机优先级来平衡树的形状。Treap结构在并发场景下具有良好的性能和可伸缩性。
Treap用于管理Go程序中的锁和信号量,以确保在多个goroutine之间正确同步和调度资源的访问。具体来说,sema.go文件中的treap结构用于存储等待某个资源的goroutine队列,并根据优先级对队列进行排序。当资源可用时,treap中具有最高优先级的goroutine将被唤醒,并允许其访问该资源。
通过使用Treap数据结构,sema.go文件中的代码能够高效地管理并发访问资源的调度和同步,同时保持队列的有序性和平衡性。
当倒数低第三位即starving=0时,即正常模式
多个协程会通过atomic原子操作(CAS)去修改最低一位,尝试从0改为1。只有一个会成功。
成功的协程拿到了锁,可以进行下一步具体的业务处理。
没有成功的协程会做多次自旋(类似for循环不断试,所以自旋堆CPU有消耗),判断这一位有没有变成0
自旋多次(其实是4次)还是0,没能改最低一位时,协程就会进入休眠,转而获取sema(sema>0时才会获取成功),协程会被记入treap中的sudog中,不会再被调度了。 同时WaiterShift字段会被更新
图片来自 Go sync.Mutex 深入不浅出,只看这一篇差不多就够了
两个问题:
如果一个等待的 goroutine 超过 1ms 没有获取锁,那么它将会把锁转变为饥饿模式。在饥饿模式下,锁的所有权将从执行 unlock 的 goroutine 直接交给等待队列中的第一个等待锁者。新来的 goroutine 将不能再去尝试竞争锁,即使锁是 unlock 状态,也不会去尝试自旋操作,而是放在等待队列的尾部。如果一个等待的 goroutine 获取了锁,并且满足以下其中一个条件:(1)它是队列中的最后一个;(2)它等待的时候小于1ms,那么该 goroutine 会将锁的状态转换为正常状态。
某个协程等待超过了1ms还没有获取到锁,那将会使锁变为饥饿模式。那当前协程执行完,下一个马上得到执行的协程,会是使锁进入饥饿模式的这个协程吗?
其实就要要搞清楚,“如果一个等待的 goroutine 超过 1ms 没有获取锁”,这个goroutine是不是一定是treap堆最头上的那个协程?还是可以是这个队列中的任何一个?
我理解应该是任何一个goroutine等待超过1ms,都会设置为饥饿模式。那此时下一个得到执行的协程是队头的。 所以不一定是使mutex变为饥饿模式的协程先得到执行
如果一个等待的 goroutine 获取了锁,并且满足以下其中一个条件:(1)它是队列中的最后一个;(2)它等待的时候小于1ms,那么该 goroutine 会将锁的状态转换为正常状态。
当(2)这种情况,是下一个获取到锁得到执行的协程,在队列中等待时间如果不超过1ms,就会把锁设置为正常模式吗? 那如果后面又有队列中的协程等待时间超过1ms,不是还要再切到饥饿模式? 或者说,正常模式和饥饿模式本身切换就很频繁?
看代码的话,大于或小于1ms,进入或退出饥饿模式,似乎都是对队头的协程而言,而不是队列中任意的一个协程…
故而上面两个问题都有比较明确的答案了~
相关演进
截至目前,已经有4版迭代优化
分别是
先来后到 –> 给新人机会 –> 多给新人些机会(新的协程没抢到的话再自旋几次试试)–> 饥饿模式(1.9新增)
官方库或知名项目中的使用
广泛使用…
更多参考:
Golang 语言中基础同步原语 Mutex 和 RWMutex 的区别
原文链接: https://dashen.tech/2019/05/08/sync包-Mutex/
版权声明: 转载请注明出处.