详解Golang互斥锁内部实现
go语言提供了一种开箱即用的共享资源的方式,互斥锁(sync.mutex), sync.mutex的零值表示一个没有被锁的,可以直接使用的,一个goroutine获得互斥锁后其他的goroutine只能等到这个gorutine释放该互斥锁,在mutex结构中只公开了两个函数,分别是lock和unlock,在使用互斥锁的时候非常简单,本文并不阐述使用。
在使用sync.mutex的时候千万不要做值拷贝,因为这样可能会导致锁失效。当我们打开我们的ide时候跳到我们的sync.mutex 代码中会发现它有如下的结构:
type mutex struct { state int32 //互斥锁上锁状态枚举值如下所示 sema uint32 //信号量,向处于gwaitting的g发送信号 } const ( mutexlocked = 1 << iota // 1 互斥锁是锁定的 mutexwoken // 2 唤醒锁 mutexwaitershift = iota // 2 统计阻塞在这个互斥锁上的goroutine数目需要移位的数值 )
上面的state值分别为 0(可用) 1(被锁) 2~31等待队列计数
下面是互斥锁的源码,这里会有四个比较重要的方法需要提前解释,分别是runtime_canspin,runtime_dospin,runtime_semacquiremutex,runtime_semrelease,
1、runtime_canspin:比较保守的自旋,golang中自旋锁并不会一直自旋下去,在runtime包中runtime_canspin方法做了一些限制, 传递过来的iter大等于4或者cpu核数小等于1,最大逻辑处理器大于1,至少有个本地的p队列,并且本地的p队列可运行g队列为空。
//go:linkname sync_runtime_canspin sync.runtime_canspin func sync_runtime_canspin(i int) bool { 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 }
2、 runtime_dospin:会调用procyield函数,该函数也是汇编语言实现。函数内部循环调用pause指令。pause指令什么都不做,但是会消耗cpu时间,在执行pause指令时,cpu不会对它做不必要的优化。
//go:linkname sync_runtime_dospin sync.runtime_dospin func sync_runtime_dospin() { procyield(active_spin_cnt) }
3、runtime_semacquiremutex:
//go:linkname sync_runtime_semacquiremutex sync.runtime_semacquiremutex func sync_runtime_semacquiremutex(addr *uint32) { semacquire(addr, semablockprofile|semamutexprofile) }
4、runtime_semrelease:
//go:linkname sync_runtime_semrelease sync.runtime_semrelease func sync_runtime_semrelease(addr *uint32) { semrelease(addr) } mutex的lock函数定义如下 func (m *mutex) lock() { //先使用cas尝试获取锁 if atomic.compareandswapint32(&m.state, 0, mutexlocked) { //这里是-race不需要管它 if race.enabled { race.acquire(unsafe.pointer(m)) } //成功获取返回 return } awoke := false //循环标记 iter := 0 //循环计数器 for { old := m.state //获取当前锁状态 new := old | mutexlocked //将当前状态最后一位指定1 if old&mutexlocked != 0 { //如果所以被占用 if runtime_canspin(iter) { //检查是否可以进入自旋锁 if !awoke && old&mutexwoken == 0 && old>>mutexwaitershift != 0 && atomic.compareandswapint32(&m.state, old, old|mutexwoken) { //awoke标记为true awoke = true } //进入自旋状态 runtime_dospin() iter++ continue } //没有获取到锁,当前g进入gwaitting状态 new = old + 1<<mutexwaitershift } if awoke { if new&mutexwoken == 0 { throw("sync: inconsistent mutex state") } //清除标记 new &^= mutexwoken } //更新状态 if atomic.compareandswapint32(&m.state, old, new) { if old&mutexlocked == 0 { break } // 锁请求失败,进入休眠状态,等待信号唤醒后重新开始循环 runtime_semacquiremutex(&m.sema) awoke = true iter = 0 } } if race.enabled { race.acquire(unsafe.pointer(m)) } } mutex的unlock函数定义如下 func (m *mutex) unlock() { if race.enabled { _ = m.state race.release(unsafe.pointer(m)) } // 移除标记 new := atomic.addint32(&m.state, -mutexlocked) if (new+mutexlocked)&mutexlocked == 0 { throw("sync: unlock of unlocked mutex") } old := new for { //当休眠队列内的等待计数为0或者自旋状态计数器为0,退出 if old>>mutexwaitershift == 0 || old&(mutexlocked|mutexwoken) != 0 { return } // 减少等待次数,添加清除标记 new = (old - 1<<mutexwaitershift) | mutexwoken if atomic.compareandswapint32(&m.state, old, new) { // 释放锁,发送释放信号 runtime_semrelease(&m.sema) return } old = m.state } }
互斥锁无冲突是最简单的情况了,有冲突时,首先进行自旋,,因为大多数的mutex保护的代码段都很短,经过短暂的自旋就可以获得;如果自旋等待无果,就只好通过信号量来让当前goroutine进入gwaitting状态。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。