【协作式原创】查漏补缺之Golang中mutex源码实现
概览最简单版的mutex(go1.3版本)
预备知识
主要结构体
type Mutex struct {
state int32 // 指代mutex锁当前的状态
sema uint32 // 信号量,用于休眠或唤醒goroutine
}
31 2 1 0
+----~~~------+-+-+
// 1.3 与 1.7 老的实现共用的常量
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexWaiterShift = iota
)
// 1.12 公平锁使用的常量
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
- state字段: 4字节int, 其中低几位用于做标记,高位地址空间用于计数,表示有多少个 goroutine 正在等待而处于休眠中。分成四部分来看:
1. [31,3]前29位: 表示有多少个 goroutine 正在等待而处于休眠中
2. [2]: 是否饥饿模式 0表示正常模式,1表示饥饿模式
3. [1]: 是否被唤醒. 0表示没有唤醒,1表示mutex已被唤醒,可以唤醒等待其它goroutine.
4. [0]: 是否加锁. 1表示加锁
mutexLocked 值为1,根据 mutex.state & mutexLocked 得到 mutex 的加锁状态,结果为1表示已加锁,0表示未加锁
mutexWoken 值为2(二进制:10),根据 mutex.state & mutexWoken 得到 mutex 的唤醒状态,结果为1表示已唤醒,0表示未唤醒
mutexStarving 值为4(二进制:100),根据 mutex.state & mutexStarving 得到 mutex 的饥饿状态,结果为1表示处于饥饿状态,0表示处于正常状态
mutexWaiterShift 值为3,根据 mutex.state >> mutexWaiterShift 得到当前等待的 goroutine 数目
- sema字段: 信号量
Lock源码-分1.3,1.7,1.12版本来讲
go1.3源码
参考附录6
- lock()
Lock方法申请对mutex加锁,Lock执行的时候,分三种情况 // 参考附录10
- 无冲突 通过CAS操作把当前状态设置为加锁状态。
- 有冲突 开始自旋(CAS函数返回false),并等待锁释放,如果其他携程在这段时间内释放了该锁,直接获得该锁;如果没有释放,进入3
- 有冲突,且已经过了自旋阶段(CAS函数返回true),执行runtime_Semacquire函数使当前携程休眠。
func (m *Mutex) Lock() {
// 情况1: CAS1直接加锁成功
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
awoke := false
for {
// 构造新的state(new),并设置Locked和Woken位,随后试图通过CAS2把m.state设置为new.
old := m.state
new := old | mutexLocked
if old&mutexLocked != 0 {
new = old + 1<<mutexWaiterShift
}
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
new &^= mutexWoken
}
// 情况2: CAS2返回false。这种一般是高并发,多个携程同时修改锁状态,导致进入自旋。
if atomic.CompareAndSwapInt32(&m.state, old, new) {// 情况3: CAS2返回true,执行下列代码
// 3.1 old本来是未加锁的(Locked从0更新为1),那么CAS2把Locked位从0置为1,就是抢锁成功,直接break返回。
if old&mutexLocked == 0 {
break
}
// 3.2 old本来就是加锁的(Locked从1更新为1),那么说明CAS2只是把waiter+1,并没有修改Locked的值,进入休眠。
runtime_Semacquire(&m.sema)
awoke = true
}
}
}
简化版:
// 情况1: CAS1直接加锁成功
if CAS1()
// 构造新的state(new),并设置Locked和Woken位,随后试图通过CAS2把m.state设置为new.
其他代码...
// 情况2: CAS2返回false。这种一般是高并发,多个携程同时修改锁状态,导致进入自旋。
for CAS2(){ // 情况3: CAS2返回true,执行下列代码
// 3.1 old本来是未加锁的,那么CAS2把Locked位从0置为1,就是抢锁成功,直接break返回。
// 3.2 old本来就是加锁的(Locked为1),那么说明CAS2只是把waiter+1,并没有修改Locked的值,进入休眠。
- unlock()
1. Locked置0
2. 构造新state(waiter-1;woken置1,表唤醒)
3. if CAS(){ // false:有冲突,Compare失败
// true:更新成功,唤醒排队的携程
}
-
Q: 什么情况会走情况2呢?
A: 高并发的情况下. 10个携程同时读取old,然后修改,只有第一个携程首先执行CAS1/CAS2成功更新state,其他9个执行CAS2时就会Compare失败,继续循环,依次类推,分别有[9,8,...,1]次Compare失败的情况。(这个是基于我的理解做的推测) -
Q: 什么情况会走情况3呢?
A: 下面用3个携程加锁解锁过程,分析state的变化.
没有人加锁,state(0000)
1. g1,抢锁成功 0000->0001
2. g2,抢锁 0001->1001, runtime_Semacquire进入阻塞
3. g3,抢锁 1001->2001, runtime_Semacquire进入阻塞
4. g1,释放锁2001->1010 [Unlock(Locked置0, waiter-1, woken置1, runtime_Semrelease唤醒休眠携程]
5. g2和g3都被唤醒,执行Lock的(awoke = true)
6. g2先执行,读取old值(1010,注意Locked是0),执行CAS2成功,则1010->1001[执行Lock的(Locked置1,waiter不变,woken置0,执行CAS2成功,break加锁成功)]。// g2就是情况3.1(Locked从0更新为1)
7. 此时g2还未释放锁。
8. g3执行,读取old值(1001,注意Locked是1),执行CAS2成功,则1001->2001[执行Lock的(Locked置1,waiter+1,woken置0,执行CAS2成功,然后runtime_Semacquire阻塞)] // g3就是情况3.2(Locked从1更新为1,并未加锁成功,所以只是waiter+1)
9. g2释放锁 2001->1010 [Unlock(Locked置0, waiter-1, woken置1, runtime_Semrelease唤醒休眠携程]
10.g3释放锁 1010->0010 [Unlock(Locked置0,判断old值的mutexLocked|mutexWoken任一为1直接return,解锁完成]
所有携程执行完毕,state变成(0010)
Q: 1.3版本的缺点
参考附录6-前言。
A: 1.3版本的CAS实现的朴素互斥锁,存在一定runtime调度浪费,即,有些携程休眠之后马上就唤醒了,不如先自旋一会儿,如果仍然不能获取锁,再休眠也不迟。
Q: 为什么1.7要引入有限自旋逻辑?
A: 减少情况3中携程的休眠情况,也就减少了休眠后又唤醒的资源开销,增加有限的资源操作,自旋仍没有获取锁,才会休眠。
增加了自旋 spinlock 的逻辑,因为大部份被mutex加锁的代码执行时间很短,自旋可以减小无谓的runtime调度。推荐看官方spin commit
Q: 什么叫自旋
A: busy-waiting,忙等状态。
1.7版本
参考附录6
1.7版本对比1.3版本
增加了自旋操作
Q: runtime_canSpin和runtime_doSpin的作用
A: 参考附录11
runtime_canSpin:通过4个条件判断是否可以自旋。
runtime_doSpin: 占着茅坑(CPU)不拉屎。
1.7版本缺点
- 不公平 (新请求锁的携程比被唤醒的携程更容易获得锁)
我个人是这么理解: 比如高并发情况下,对商品数量n加锁.
- 第一次有3个请求,形成3个携程,g1成功获取锁,g2,g3堵塞排队。
- 然后又来3个请求,形成3个携程,g4,g5,g6,且还未阻塞。
- g1释放锁, g2被唤醒,和g456同时争抢锁,g456抢到锁的概率是更大的。(新请求锁的携程存在一个优势,它们已经在CPU上运行且它们数量很多) // 参考附录10
为了解决公平性问题,1.9版本引入了饥饿模式。
1.12版本 暂时略
饥饿模式与正常模式
参考附录10和附录6
正常模式: 解锁后,被唤醒的携程和新来的携程会一起竞争锁,由于新来的携程在CPU上运行且数量很多,所以一般是新来的携程竞争成功,所以唤醒队列里面的携程容易饥饿。
饥饿模式: 新来的携程不参与竞争。
饥饿模式切回正常模式: 略。
mutex源码总结和借鉴
- mutex底层是通过CAS实现的,演进了3个版本,第二版增加了有限自旋,减少了携程的阻塞和唤醒, 第三版增加了饥饿模式,从不公平的锁变为公平锁。
- 锁模式和公平性
- iota和4个位操作(设置,清空,查询,左移右移):给标志位设置1(|或操作),清空标志位(&^按位清除(a与上非b)操作),获取标志位的值(&与操作)
剩余部分,看时间决定是否讲
Q: go有哪些锁
A:
- 所有锁的都可以归为4大类: 1. 共享锁,独占锁 2. 悲观锁,乐观锁
- 按照这个分类方法,sync.Mutex(独占锁,互斥锁,悲观锁),sync.RWMutex(共享锁)
Q: 什么是自旋锁
Q: go实现自旋锁
type spinLock uint32
func (sl *spinLock) Lock() {
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
runtime.Gosched()
}
}
func (sl *spinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
func NewSpinLock() sync.Locker {
var lock spinLock
return &lock
}
Q: 实现可重入的自旋锁
思路: 要使得一个锁可以多次Lock和unlock,给自旋锁增加2个字段即可:
- owner:持有该自旋锁的携程Id.
- count:持有锁的携程的加锁次数. 除了第一次加锁是调用cas加锁,剩下的加锁都是通过对count+1进行伪加锁.
应用场景: 附录9
参考资料
1.图解CAS
2.GO中CAS代码示例
3.CAS乐观锁,最精确的文字描述.
4.CAS会导致"ABA问题"
5.Go-iota使用例子
6.sync.Mutex的实现和演进
7.Go语言中你所不知道的位操作用法 TODO: 通过位操作实现用户类型的存储,更新和修改。如[一个qq号可以用VIP会员,SVIP超级会员,蓝钻用户,黄钻用户,红钻用户....]
8.golang 自旋锁
9.可重入锁的使用场景 TODO:
10.go夜读-sync.Mutex 源码分析
11.runtime_canSpin和runtime_doSpin
Go中锁的那些姿势,估计你不知道
TODO
乐观锁中,我们有3种常用的做法来实现:
https://www.cnblogs.com/cwfsoft/p/7759944.html
cas实现lockfree才会有ABA问题。ABA不是cas自己的问题。这是两回事。
cas理论上还分weak和strong。