天下之事常成于困约,而败于奢靡。——陆游
1 前言
互斥锁是并发程序中对共享资源进行访问控制的主要手段,因此Go设计者提供了非常简单易用的Mutex供我们使用,接下来我们从源码剖析实现原理,又不会过分纠结于实现细节。
2 Mutex数据结构
2.1 结构体定义
type Mutex struct { state int32 sema uint32 }
- state表示互斥锁的状态,比如是否被锁定等
- sema表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
state是32位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态,如下图:
- Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
- Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。
- Starving:表示该Mutex是否处于饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。
- Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
协程之间抢锁实际上是抢给Locked赋值的权利,能给Locked字段置为1,就说明抢锁成功。抢不到的话就阻塞等待sema信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒。
Woken和Starving主要用于控制协程间的抢锁过程。
2.2 方法
Mutext对外提供两个方法:
- Lock() : 加锁方法
- Unlock(): 解锁方法
下面我们分析一下加锁和解锁的过程,加锁分成功和失败两种情况,成功的话直接获取锁,失败后当前协程被阻塞,同样,解锁时跟据是否有阻塞协程也有两种处理。
3 加锁
3.1 简单加锁
假如当前只有一个goroutine在加锁,没有其他goroutine干扰,那么加锁过程如下图:
加锁过程会去判断Locked标志位是否为0,如果是0则把Locked位置1,代表加锁成功。从上图可见,加锁成功后,只是Locked位置1,其他状态位没发生变化。
3.2 加锁被阻塞
假如加锁时,锁已经被其他goroutine占用了,此时加锁过程如下图:
从上图可看到,当goroutine-B对一个已被占用的锁再次加锁时,Waiter计数器增加1,此时goroutine-B将被阻塞,直到Locked值变为0后才会被唤醒。
4 解锁
4.1 简单解锁
假如解锁时,没有其他goroutine阻塞,此时解锁过程如下图:
由于没有其他goroutine阻塞等待加锁,所以此时解锁时只需要把Locked位置为0即可,不需要释放信号量。
4.2 解锁并唤醒协程
假如解锁时,有1个或多个goroutine阻塞,此时解锁过程如下图:
goroutine-A解锁过程分为两个步骤,一是把Locked位置为0,二是查看到Waiter>0,所以释放一个信号量,唤醒一个阻塞的goroutine,被唤醒的goroutine-B把Locked位置为1,于是goroutine-B获得锁。
5 小结
目前没有涉及到大量源码,因为mutex的源码本身比较多,由于篇幅关系这里只是提取了mutex的实现原理,但是还有部分细节准备在下次分享中继续讨论,比如自旋,工作模式以及饥饿模式等。
6 关注公众号
微信公众号:堆栈future