大家好,我是「云舒编程」,今天我们来聊聊Golang锁结构:Mutex。
文章首发于微信公众号:云舒编程
关注公众号获取:
1、大厂项目分享
2、各种技术原理分享
3、部门内推
一、前言
书接上回,在万字图解| 深入揭秘Golang锁结构:Mutex(上)一文中,我们已经研究了Golang mutex V1和V2版本的实现。接下来我们继续研究V3和V4版本的实现。
二、面试中遇到Mutex
为了让剧情顺利发展,我们依旧使用万字图解| 深入揭秘Golang锁结构:Mutex(上)一文中的面试对话模式。
面试官:你现在实现的锁的确给了新来的Goroutine直接获取锁的机会,但是还不够优雅。比如说,新Goroutine尝试获取锁失败的那一刻,锁就被释放了,但是新Goroutine需要等到下一次信号量唤醒加调度才有机会再次获取锁,这样其实浪费了新Goroutine的CPU时间,你可以再优化下吗?
我:考虑到这种情况,可以尝试给新的Goroutine多次获取锁的机会,说白了就是允许自旋,但是需要给自旋加一些限制条件,避免最开始提到的性能问题。
面试官:需要加哪些限制条件呢?
我:首先需要限制自旋的次数,其次操作系统的处理器个数和Golang 调度的P个数都必须大于1,否则就会是串行,自旋就没有意义了。
面试官:不错,怎么实现呢?
我:我来写下:
const (
mutexLocked = 1 // mutex is locked
mutexWoken = 2
mutexWaiterShift = 2
)
type Mutex struct {
state int32
sema uint32
}
func (m *Mutex) Lock() {
//给新来的协程直接加锁的机会
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
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
}
//内部调用procyield函数,该函数也是汇编语言实现。
//函数内部循环调用PAUSE指令。减少cpu的消耗,节省电量。
//指令的本质功能:让加锁失败时cpu睡眠30个(about)clock,从而使得读操作的频率低很多。流水线重排的代价也会小很多。
runtime_doSpin()
iter++
continue
}
new = old + 1<<mutexWaiterShift //锁没有释放,当前协程可能会阻塞在信号量上,先将waiter+1
}
··· //剩下的不变
}
}
//判断是否可以自旋,同时满足以下4个条件才能自旋:
//1、自旋次数小于4次
//2、