推荐阅读
锁的最佳实践
- 减少持有时间,缩小临界区
- 优化锁的粒度, 空间换时间
- 读写分离,读写锁 & sync.Map
- 使用原子操作,无锁数据结构
锁小结
- 不要拷贝Mutex, 要拷贝就必须拷贝指针
- Mutex 不可重入,容易死锁
- atomic.Value应存入只读对象
- race detector工具用于检测并发问题
锁的进化
1. 单核时代
单核时代,只需要屏蔽中断,就可以实现原子操作
但是系统调用性能太低
2. 近代:CAS
CMPXCHG DX,val 硬件级别的原子操作,compare and swap,CAS
CAS汇编:
LOCK前缀 = 锁定内存总线
低效:内存总线成为瓶颈
3. 现代:MESI
MESI缓存一致性
MESI協議是一個基於失效的緩存一致性協議,是支持寫回(write-back)緩存的最常用協議。也稱作伊利諾伊協議(Illinois protocol,因為是在伊利諾伊大學厄巴納-香檳分校被發明的)。與寫穿(write through)緩存相比,回寫緩衝能節約大量帶寬。總是有「髒」(dirty)狀態表示緩存中的數據與主存中不同。MESI協議要求在緩存不命中(miss)且數據塊在另一個緩存時,允許緩存到緩存的數據複製。與MSI協議相比,MESI協議減少了主存的事務數量。這極大改善了性能。
状态
緩存行有4種不同的狀態:
已修改Modified (M)緩存行是髒的(dirty),與主存的值不同。如果別的CPU內核要讀主存這塊數據,該緩存行必須回寫到主存,狀態變為共享(S)。
獨占Exclusive (E)緩存行只在當前緩存中,但是乾淨的(clean)--緩存數據同於主存數據。當別的緩存讀取它時,狀態變為共享;當前寫數據時,變為已修改狀態。
共享Shared (S)緩存行也存在於其它緩存中且是乾淨的。緩存行可以在任意時刻拋棄。
無效Invalid (I)緩存行是無效的
任意一對緩存,對應緩存行的相容關係:
M | E | S | I | |
---|---|---|---|---|
M | X | X | X | √ |
E | X | X | X | √ |
S | X | X | √ | √ |
I | √ | √ | √ | √ |
當塊標記為 M (已修改), 在其他緩存中的數據副本被標記為I(無效).
4. 自旋锁
linux内核中常见,基于CAS实现原子操作
适用于等待时间比较短的情况
实现
上述代码, 版本必须大于等于1.14
注意Go 1.14 之前版本无非抢占式调度,会导致死锁
1.14之后有抢占式调度了,没问题
原因分析:
1.14 之前版本无非抢占式调度,Go的调度器无法主动挂起一个goroutine,只能等到协程主动交出控制权,如果发送GC,需要stop the world,如下图
性能: 多个goroutine被同时唤醒,浪费CPU
公平: 可能抢不到锁(饥饿),导致p99耗时毛刺
5. Mutex
设计思路:效率优先,兼顾公平
效率: 正常模式
等待队列:先进先出
新手优势:先抢再排
等待超1ms -> 饥饿模式
公平: 饥饿模式
严格排队:队首接盘
牺牲效率:保P99
适时回归 正常模式
为什么正常模式效率更高?
- 因为正常模式新任务进来不需要进入等待队列,减少调度开销
- Goroutine 可以充分利用缓存(操作系统内存模型)
实现原理
CAS + 等待队列 + 正常/饥饿模式
state
bits | 31~3 | 2 | 1 | 0 |
---|---|---|---|---|
用途 | 等待队列长度 | 0 = 正常 1 = 饥饿 | 0 = 无唤醒 1 = 有唤醒 | 0 = 解锁 1 = 上锁 |
state = 0: 空等待队列,正常模式,无唤醒,解锁。 Mutex的初始状态 或被最后一个goroutine解锁