推荐阅读


锁的最佳实践

  1. 减少持有时间,缩小临界区
  2. 优化锁的粒度, 空间换时间
  3. 读写分离,读写锁 & sync.Map
  4. 使用原子操作,无锁数据结构

锁小结

  1. 不要拷贝Mutex, 要拷贝就必须拷贝指针
  2. Mutex 不可重入,容易死锁
  3. atomic.Value应存入只读对象
  4. 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)緩存行是無效的

任意一對緩存,對應緩存行的相容關係:

MESI
MXXX
EXXX
SXX
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

适时回归 正常模式

为什么正常模式效率更高?

  1. 因为正常模式新任务进来不需要进入等待队列,减少调度开销
  2. Goroutine 可以充分利用缓存(操作系统内存模型)


实现原理

CAS + 等待队列 + 正常/饥饿模式

state

bits31~3210
用途等待队列长度0 = 正常
1 = 饥饿
0 = 无唤醒
1 = 有唤醒
0 = 解锁
1 = 上锁

state = 0: 空等待队列,正常模式,无唤醒,解锁。 Mutex的初始状态 或被最后一个goroutine解锁