Golang sync.Mutex 深度剖析(一)
2023-01-02
之前的有小伙伴提到了“当提到并发编程、多线程编程时,往往都离不开锁这一概念”,对此我深有同感。当对应场景发生时,我们可能会使用 singleflight、sync.Once 等高级封装简化代码编写,提高工作的效率,而这两个类库底层都是调用 sync.Mutex 包实现。目前网络上有不少关于这个包的讲解博文,但大多数趋于讲述怎么调用,在这里我把我学习 sync.Mutex 包的过程以及心得体会整理分享出来,带领大家探究一下它是如何实现的吧。
一、首先看看这个包的作用是什么
包如其名,这个包是 Go 语言中对锁的封装,是一个非常简单且“暴力”的互斥锁。当一个 goroutine 对某一个“片段”上锁后,其他 goroutine 想要再次上锁时就只能“阻塞”住乖乖等它释放。
二、基本组成
Mutex 结构体包含了 state、sema 两个成员;state 表示锁当前的状态,是一个复合属性,可以粗暴把他看做一个由 0/1 组成的二进制 bitmap。从右边数起,第一位表示当前这个锁是不是处于上锁状态(0:没有锁定 1:已被锁定), 第二位标识着在等待的 goroutine 是否已被唤醒(0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中);第三位标识是否有 goroutine 处于饥饿状态。如果一个等待的 goroutine 超过1ms没有获取锁,那么它将会把锁转变为饥饿模式,饥饿模式下竞争的表现会变动稍微不同,这里后续将会详细讲解。剩余的29位则作为一个简单的计数器,表示当前正在等待的 goroutine 个数。sema 则表示信号量,(是的就是你想象中的那个进程间通信的信号量),用于协程之间的协调。
同时,它实现了 sync.Locker 这个接口,拥有 Lock(上锁)、Unlock(解锁) 能力。
// A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. type Mutex struct { state int32 sema uint32 } // A Locker represents an object that can be locked and unlocked. type Locker interface { Lock() Unlock() } |
三、包的发展历程
辩证法教会我们,要用发展的眼光看待问题,编程语言也不例外。sync.Mutex 这个包从诞生至今经历过 4 次重大的变革,变得越来越“快”,变得越来越合理“合理”。这里借用一张网上的图,很好的解释了这 4 步发展历程。
在第一个阶段,Mutex 基于信号量实现了简单的互斥锁,用一句很俗套的话来说,就是“该有的都有了”,上层封装也非常简单,这里摘录一段源码如下:
|
咋看是不是很有我们大学上“操作系统”课时,写出来的作品?用一个变量表示有没有上锁,上锁时通过 acquire 获取信号量,解锁时 release,简洁,实用!
肯定有小伙伴发现了,Mutex 的解锁有一个神奇之处,在设计上它允许任何协程去执行 Unlock 操作,并不关心是不是上锁的和解锁的是不是同一个协程。这么设计有好有坏,在使用上还是需要关注一下这一点的。
此外单纯用系统的信号量实现的锁,唤醒 goroutine 的机制是按排队顺序,谁在前面就先唤醒谁。看着没啥问题,但是这其中却存在着一个 CPU 上下文切换的过程,不一定是排着队的那个 goroutine 就是被唤起效率就是最高的(你想想看,如果这时候正好有个 goroutine 正在上锁,就这么分毫之差内还得去阻塞排队,万一这个 goroutine 内的操作极其简单,上下文切换的性能损耗说不定比这个 goroutine 本身的还大 ),因此执行在高并发的情况下还是会有不容忽视的损耗,这也就来到了第二版 Mutex。
第二、第三版 Mutex 整体比较类似,整体思路都是被唤醒后,和新进来的 goroutine 再一起抢一轮锁,具体请听下回分解。
参考资料:
- mutex 源码:https://github.com/golang/go/blob/d90e7cbac65c5792ce312ee82fbe03a5dfc98c6f/src/pkg/sync/mutex.go
- cas 的汇编语言源码:https://github.com/golang/go/blob/weekly.2011-01-20/src/pkg/sync/asm_amd64.s
- 信号量相关 C 语言源码:https://github.com/golang/go/blob/d90e7cbac65c5792ce312ee82fbe03a5dfc98c6f/src/pkg/runtime/sema.c
- sync_runtime_canSpin源码:https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/runtime/proc.go#L5580