Go sync.Mutex 解析
Contents
简介
sync.Mutex
是 go 标准库中使用的排它锁。当一个 goroutine 获取锁后,其它 goroutine 则无法获取锁而被阻塞,直到锁被释放而成功抢到锁。
本文是对 sync.Mutex
源码的总结,不会涉及到具体代码逻辑。
两方面的取舍
sync.Mutex
的实现主要涉及两方面的取舍
抢锁策略
排它锁是独占资源,因此会存在两个 goroutine 同时抢同一个锁的情况,不同的抢锁策略会导致不同的 goroutine 抢到锁。有如下两类抢锁策略
自旋
当进程 A 在获取锁时,若锁已经被抢占,则进程 A 将不断循环、判断锁是否能够被成功获取,直到成功抢到锁才退出循环。
|
|
信号量阻塞
进程抢不到锁,则通过信号量操作被系统挂起。待锁释放时,系统根据进程挂起的先后顺序,FIFO 唤醒进程去抢锁。
|
|
两种抢锁策略各有优劣,如下表
优点 | 缺点 | |
---|---|---|
自旋 | 轮询抢锁,抢锁速度快 | 长时间轮询,对 CPU 资源消耗较大 |
信号量阻塞 | 对 CPU 资源的消耗较少;FIFO 的抢锁顺序,较为公平 | 系统唤醒进程去抢锁,唤醒有一定成本;唤醒需要一定时间,抢锁速度较慢 |
公平性
在 sync.Mutex
中,为了充分利用自旋
、信号量阻塞
两种抢锁策略的优点,采用了 “先自旋一定次数,后信号量阻塞” 的方式来抢锁,伪代码如下:
|
|
但这种抢锁方式会造成 goroutine 饿死,例如这个 case
|
|
goroutine A、B 在争抢一把锁。开始时两者均使用自旋的方式抢锁,假设 A 先抢到了锁,那么 B 自旋次数用完后就使用信号量阻塞抢锁,并被阻塞而进入挂起状态。
当 goroutine A 释放锁后,协程 B 被唤醒,此时 A 也接着开始抢锁。由于进程(协程对应的)被唤醒需要一定时间,因此有很大几率是晚于 A 开始抢锁的,因此很大概率上还是 A 抢到锁。
长此以往,协程 B 一直抢不到锁,就是我们说的协程 B 被 “饿死” 了。
因此,为避免”饿死“情况的出现,保证每个协程都能尽量公平地拿到锁,go 在 1.9 版本后加入了 “饥饿模式” 来避免饿死的情况出现。在不同的模式中抢锁步骤略有不同,如下表
抢锁步骤 | 优缺点 | |
---|---|---|
正常模式 | 先自旋指定次数,然后信号量阻塞排队抢锁 | 兼顾性能,能较快地抢到锁;但是可能出现”饿死“情况 |
饥饿模式 | 信号量阻塞排队抢锁 | 抢锁速度较慢;但能较好地保证公平性,不会有“饿死”的情况 |
Go 中对抢锁耗费的时间进行了记录,若抢锁耗费的时间超过 1ms,则将当前模式从 “正常模式” 切换到 “饥饿模式” ;当饥饿协程获取锁后,才切换回 “正常模式”
总结
了解了 sync.Mutex
库的基本逻辑,这些问题也就不难回答了
- 使用全局一个大锁 or 多个小锁,哪个更好
- 为什么
sync.Mutex
结构不能被复制使用
参考资料
https://morioh.com/p/0a103ab09f46
Author Jakseer
LastMod 2022-01-15