sync.Mutex是一个不可重入的排他锁。当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放。
sync.Mutex数据结构
type Mutex struct {
//状态
state int32
//控制锁状态的信号量
sema uint32
}
const (
//锁的状态
mutexLocked = 1 << iota // mutex is locked
//正常模式唤醒
mutexWoken
//进入饥饿模式
mutexStarving
mutexWaiterShift = iota
)
sync.Mutex锁的模式
正常模式:所有等待锁的goroutine按照顺序等待
饥饿模式:新来的goroutine不会去尝试获取锁,也不会进行自旋操作,会被放到等待队列的尾部
sync.Mutex断点调试
首先先起两个goroutine在x.Lock()处下断点然后debug运行
Lock
这里可以看到我们刚刚2个goroutine一个获取到锁return一个进入lockSlow函数
接下来让我们看一下lockSlow函数
一步步点击红箭头指向的按钮,会进入到for循环接下来让我们一点点看for循环里面的逻辑
//是否满足自旋条件函数
func sync_runtime_canSpin(i int) bool {
// i >= active_spin 当前go协程自旋状态小于4次 active_spin值为4
//ncpu <= 1 当前电器cpu核数大于1
//GOMAXPROCS>1,并且至少有一个正在运行P,而本地runq为空
if i >= active_spin || ncpu <= 1 || gomaxprocs int32(sched.npidle+sched.nmspinning)+1 {
return false
}
if p := getg().m.p.ptr(); !runqempty(p) {
return false
}
return true
}
func (m *Mutex) lockSlow(){
//goroutine等待时间
var waitStartTime int64
//是否是饥饿模式
starving := false
//goroutine是否被唤醒
awoke := false
//自旋次数
iter := 0
old := m.state
for {
//这里运用位操作
//第一个条件:锁处于被锁状态并且不处于饥饿状态
//第二个条件:是否满足自选条件 runtime_canSpin()函数
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
此案例m.status的状态只有1 2 3
status 1 锁还没有被释放,锁处于正常状态
status 2 未解锁 但处于饥饿模式
status 3 锁被释放处于饥饿模式
status 4 锁已经被释放, 锁处于饥饿状态
// new 用来设置新的状态
// old 是锁当前的状态
new := old
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 将等待队列的等待者的数量加1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
//切换的饥饿模式,如果已经解锁则不切换
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
//锁已经被释放
if old&(mutexLocked|mutexStarving) == 0 {
break
}
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
//获取goroutine等待时间
waitStartTime = runtime_nanotime()
}
//queueLifo = false 表示新来的goroutine放到队列后面等待
//queueLifo = true 唤醒的goroutine放到队列第一位
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
//进入饥饿模式
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
//锁置为正常模式
if !starving || old>>mutexWaiterShift == 1 {
//退出饥饿模式
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
//唤醒go协程
awoke = true
//自旋次数置为0
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
接下来我们一直调试断点会发现如下状态
我们会发现选择goroutine下拉框比上面少了goroutine6这是因为goroutine6运行完成释放了锁,这是正好对应status 3 锁被释放处于饥饿模式
接下来在调试就会获得锁break跳出循环
Unlock
Unlock相对Lock要简单
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
//把状态置未0
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
sync.Mutex.unlockSlow 方法开始慢速解锁
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
//不处于饥饿模式
if new&mutexStarving == 0 {
old := new
for {
//如果mutexLocked、mutexStarving、mutexWoken 都不等于0直接return
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
//如果存在等待的,调用runtime_Semrelease移交控制权
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
//处于饥饿模式直接,调用runtime_Semrelease移交控制权
runtime_Semrelease(&m.sema, true, 1)
}
}
总结
status有四种状态
status 1 锁还没有被释放,锁处于正常状态
status 2 未解锁 但处于饥饿模式
status 3 锁被释放处于饥饿模式
status 4 锁已经被释放, 锁处于饥饿状态
自旋条件
(1) i >= active_spin 当前go协程自旋状态小于4次 active_spin值为4
(2) ncpu <= 1 当前电器cpu核数大于1
(3)GOMAXPROCS>1,并且至少有一个正在运行P,而本地runq空
(4)不处于饥饿模式
Lock
1.正常模式
判断是否满足自旋条件,处理完自旋条件,保存锁的状态到new,并根据mutexStarving.mutexLocked这几个不同的条件更新new的值,然后通过CAS更新锁的状态置,如果这时候锁的状态等于0并且不处于饥饿状态,则获取锁成功返回,通过runtime_nanotime获取等待时间调用runtime_SemacquireMutex保障不会被同事获取
2.饥饿模式
饥饿模式下,没有自旋逻辑,新进来的goroutine会被放在队列最后面
Unlock
1.把锁的状态置为0,如果正常则释放锁成功
2.如果释放锁失败则会调用unlockSlow慢解锁:
(1) 正常模式下如果mutexLocked、mutexStarving、mutexWoken 都不等于0直接return,如果存在等待的,调用runtime_Semrelease移交控制权
(2)饥饿模式下直接调用runtime_Semrelease移交控制权