sync包-Mutex

推荐 多图详解Go的互斥锁Mutex


初入门径


// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
//
// In the terminology of the Go memory model,
// the n'th call to Unlock “synchronizes before” the m'th call to Lock
// for any n < m.
// A successful call to TryLock is equivalent to a call to Lock.
// A failed call to TryLock does not establish any “synchronizes before”
// relation at all.
type Mutex struct {
state int32
sema uint32
}

其中 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版迭代优化

Go sync.Mutex 深入不浅出–版本迭代

分别是

先来后到 –> 给新人机会 –> 多给新人些机会(新的协程没抢到的话再自旋几次试试)–> 饥饿模式(1.9新增)




官方库或知名项目中的使用


广泛使用…




更多参考:

go中semaphore源码解读

Go精妙的互斥锁设计

golang-浅析mutex

Go语言sync包的应用详解

semaphore 的原理与实现

Golang 语言中基础同步原语 Mutex 和 RWMutex 的区别

手摸手Go 并发编程基建Semaphore

缓存击穿导致 golang 组件死锁的问题分享

GO: sync.Mutex 的实现与演进

Golang同步机制的实现

golang-浅析mutex