互斥锁的定义
type Mutex struct { state int32 sema uint32 }
一个 sema,背后实际上 是一个 休眠队列,可以看下上篇。
一个state,这个状态 分为4个部分。
后三位 各自代表一个状态。 前29位代表最大可等待协程的个数。
state的结构
locked 是否加锁 1加锁,0 正常 占1位 woken 是否醒来 占1位 starving 是否饥饿模式 占1位 waiterShift 等待的数量 占29位
底层的定义,下面看代码时候,会说明。
正常模式加锁
假设现在来了2个g,都想加锁,但是只有一个能成功,2个都通过 atomic.CompareAndSwapInt32(lock, 0 ,1) 伪代码
去更改 locked 位置。
改成功的g获取了锁,没成功的g先自旋几次,然后如果还是未获取到锁,则进入
sema
休眠队列。
未成功的g进入休眠队列,把waiterShift加1。
通过这个结论,看代码验证下:
mutexLocked = 1 < starvationThresholdNs}}
小结:
尝试CAS直接加锁若无法直接获取,进行多次自旋尝试多次尝试失败,进入sema队列休眠
如果这个时候,再来一个:
也是同样,进入sema的休眠队列。
解锁
解锁的这个g
,除了修改locked
的值,还需要去判断waiterShift
,有没有协程在等,如果有,要去唤醒一个协程。
看代码:
func (m *Mutex) Unlock() { // 减去1,发现state的值,不是0,说明有协程在等new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {m.unlockSlow(new)}}func (m *Mutex) unlockSlow(new int32) {if new&mutexStarving == 0 { // 这里是讲了 非饥饿模式old := newfor {new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1) // 从 sema中释放一个 greturn}old = m.state}}
正常模式比较好理解:
如果一个g先加锁成功,则别的g进来后,先自旋等待一下,然后进入sema休眠队列。
等到g解锁时候,回去释放sema休眠队列中的一个g,这个队列是平衡树。
mutex正常模式:自旋加锁+sema休眠等待
饥饿模式
假设g解锁后,释放了一个g出来。现在 mutex
的locked
位置为0
。 这个时候,又来了2个g,那刚刚释放的g不一定能竞争得过来的这两个g。
为了解决这个问题,go设置了锁饥饿模式:
当前协程等待锁的时间超过了 1ms,切换到饥饿模式饥饿模式中,不自旋,新来的协程直接sema休眠饥饿模式中,被唤醒的协程直接获取锁没有协程在队列中继续等待时,回到正常模式
把starving置为1
新过来的协程直接休眠,唤醒的协程直接获得锁
代码: 有点长 要结合里面的for循环,看两遍func (m *Mutex) lockSlow() {var waitStartTime int64starving := falseawoke := falseiter := 0old := m.statefor {// 饥饿模式不自旋了if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {runtime_doSpin()iter++ // 自旋次数加1old = m.statecontinue}new := old// 判断是否是饥饿模式if old&mutexStarving == 0 {new |= mutexLocked}// 如果是饥饿模式,给waiterShift 加1if old&(mutexLocked|mutexStarving) != 0 {new += 1 < starvationThresholdNsold = m.stateif old&mutexStarving != 0 {delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 {delta -= mutexStarving}// 直接改为 g已经获取锁的值,直接写入atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}}
总结:
锁竞争严重时,互斥锁进入饥饿模式饥饿模式没有自旋等待,有利于公平,见过有人叫 公平锁 。
使用经验
1. 减少锁的使用时间,lock和unlock 之间,业务要精简,只放必须的代码。 2. 善用defer确保锁的释放。避免忘记释放 例如走到if这样分支,最后没有释放锁。
思考一个问题:
加锁、开锁其实就是用的 atomic 操作一个 值,开发者也能实现,为什么还要用锁 ?
结合上几篇讲的 sema 和 协程抢占的内容,这样做是能够做到锁住一段代码,但是,未获取锁的g,无法做到休眠、唤醒的功能。 所以,系统才的 mutex 采用 atomic和 sema的结合。