斥锁是并发程序中对共享资源进行访问控制的主要手段.Go语言提供了非常简单易用

的Mutex.Mutex为结构体类型.对外暴露了Lock()和Unlock()方法.用于加锁和解锁.

1.Mutex数据结构:

源码位置src/internal/sync/mutex.go:Mutex.

// A Mutex is a mutual exclusion lock.
//
// See package [sync.Mutex] documentation.
type Mutex struct {
	state int32
	sema  uint32
}

Mutex.state表示互斥锁的状态.比如锁是否被锁定.

Mutex.sema表示信号量.协程阻塞等待该信号量.解锁的协程释放信号量从而唤醒等

待信号量的协程.

2.Mutex内存布局:

Locked:表示Mutex是否已被锁定.0表示没有锁定.1表示已锁定.

Woken:表示是否有协程已被唤醒.0表示没有协程唤醒.1表示已有协程唤醒.正在加锁

过程中.

Starving:表示Mutex是否处于饥饿状态.0表示没有饥饿.1表示饥饿.说明有协程阻

塞了超过1ms.

Waiter:表示阻塞等待锁的协程个数.协程解锁时根据此值来判断是否需要释放信号

量.协程之间的抢锁实际上是抢给Locked赋值的权利.能给Locked域置1.就说明抢锁

成功.抢不到就阻塞等待Mutex.sema信号量.一旦持有锁的协程解锁.等待的协程就

会依次被唤醒.

3.Mutex对外方法:

Mutex对外提供的方法主要是加锁和解锁.

Lock():加锁方法.

Unlock():解锁方法.

TryLock():以非阻塞方式尝试加锁(Go1.18引入).

4.加解锁过程:

1).简单加锁:

加锁过程会判断Locked标记位是否为0.如果是0则把Locked位置为1.代表加锁成功.

由上图可知.加锁成功后.只是Locked变为1.其他状态为没有变化.

2).加锁被阻塞:

假定加锁时.锁被其他线程占用了.如下图所示:

由上图可知.当协程B对一个已被占用的锁再次进行加锁时.Waiter计数器增加了1.此

时协程B将被阻塞.直到Locked为0才会被唤醒.

3).简单解锁:

假定解锁时.没有其他协程阻塞.过程如下图.

由于没有其他协程阻塞等待加锁.所以此时解锁只需要把Locked位置置为0即可.不需

要释放信号量.

4).解锁并唤醒协程:

协程A解锁分为两个步骤.一是把Locked置为0.然后是查看Waiter>0.然后释放一个

信号量.唤醒一个阻塞的协程.被唤醒的协程把Locked置为1.于是协程B获得锁.

5.自旋过程:

加锁时如果当前Locked位为1.则说明该锁当前由其他协程持有.尝试加锁的协程并不

能马上转入阻塞.而是会持续的探测Locked位是否变为0.这个过程称为自旋.自旋的

时间很短.如果在自旋的过程中发现锁已经被释放.那么协程立即获取锁.此时即便有协

程被唤醒也无法获取锁.只能再次阻塞.

自旋的好处是.当加锁失败时不必立即转入阻塞.有一定机会获取锁.可以避免协程的切

换.

1).什么是自旋:

自旋对应于CPU的PAUSE指令.CPU对该指令什么都不做.相当于CPU空转.对程序而

言相当于"sleep"了一段时间.时间非常短.当前实现是30时钟周期.

自旋过程会持续探测Locked是否变为0.连续两次探测间隔就是在执行这些PAUSE指

令.它不同于sleep.不需要将协程转为睡眠状态.

2).自旋条件:

加锁过程时会自动判断是否可以自旋.无限制的自旋会给CPU带来巨大的压力.

1.自旋次数要足够小.通常为4.即自旋最多为4次.

2.CPU核数要大于1.否则自旋没有意义.因为此时不可能有其他协程释放锁.

3.协程调度机制中的Processor数量要大于1.比如使用COMAXPROCS()将处理器

设置为1就不能启用自旋.

4.协程调度机制中的可运行队列必须为空.否则会延迟调度.

3).自旋的优势:

更充分的利用CPU.尽量避免协程切换.因为当前申请加锁的过程拥有CPU.如果经过

短时间的自旋可以获取锁.则当前协程可以继续运行.不必进入阻塞状态.

4).自旋的问题:

如果自旋过程中获得锁.那么之前被阻塞的协程将无法获得锁.如果加锁的协程特别多.

每次都通过自旋获得锁.会导致之前被阻塞的进程将很难获得锁.从而进入Straving.

为了避免协程长时间无法获取锁.自1.8版本以来增加了一个状态.Mutex的

Straving.在这个状态下不会自旋.一旦有协程释放锁.一定会唤醒一个协程加锁成功.

6.Mutex模式:

每个Mutex都有两个模式.称为Normal和Straving.

1)Normal模式:

默认情况下.Mutex模式为Normal.

在该模式下.如果协程加锁不成功不会立即转入阻塞排队.而是会判断是否满足自旋条

件.如果满足则会启动自旋过程.尝试抢锁.

2).Straving模式(也称作饥饿模式):

自旋过程中能抢到锁.一定意味着同一时刻有协程释放了锁.释放锁如果发现有阻塞等

待的协程.那么还会释放一个信号量来唤醒一个等待协程.被唤醒的协程得到CPU后开

始运行.发现锁已经被抢占了.自己再次阻塞.阻塞之前会判断自己阻塞了多长时间.如

果超过1ms.将会把Mutex标记位Straving模式.然后阻塞.

7.Woken状态:

Woken状态用于加锁和解锁过程的通信.同一时刻.两个协程一个在加锁.另一个在解

锁.加锁的协程可能在自旋中.此时把Woken状态标记位1.通知解锁协程不用释放信

号量了.好比再说.你只管解锁.不必释放信号量.我马上拿到锁了.

8.为什么重复解锁会触发panic:

从Unlock释放可以理解.Unlock分为将Locked置为0和判断Waiter值两个过程.如

果Waiter>0.释放信号量.多次执行Unlock会释放多个信号量.这样会唤醒多个协程.

多个协程在Lock()逻辑中抢锁.会增加Lock()逻辑的复杂性.也会引起不必要的协程切

换.

9.Tips:

1).使用defer避免死锁

加锁后立即使用defer对其解锁.可以有效的避免死锁.

2)加锁和解锁应该成对出现.

加锁和解锁最好出现在同一个层次的代码块中.比如同一个函数.

重复解锁会引起panic.应该避免出现这种操作.

为你.千千万万遍.

如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐