|
| 1 | +# 锁 |
| 2 | + |
| 3 | +## 实现方式 |
| 4 | +基于信号量来实现 |
| 5 | + |
| 6 | +## 信号量 |
| 7 | +### runtime_Semacquire:协程阻塞与休眠 |
| 8 | +```go |
| 9 | +// 阻塞当前协程,直到信号量 s 被释放 |
| 10 | +// lifo 控制唤醒顺序(true 表示后进先出,用于自旋优化) |
| 11 | +// skipframes 表示堆栈跟踪跳过的帧数 |
| 12 | +func runtime_Semacquire(s *uint32, lifo bool, skipframes int) |
| 13 | +``` |
| 14 | +#### 核心逻辑 |
| 15 | +- 快速检查:先尝试原子操作减少信号量值,若成功则直接返回(无需阻塞)`atomic.Xadd(s, -1) >= 0` |
| 16 | +- 加入等待队列:创建 sudog 结构体,表示当前等待的协程,将 sudog 加入与信号量关联的队列。 |
| 17 | +- 进入休眠:调用操作系统原语让出 CPU,进入阻塞状态,Linux:通过 futex(FUTEX_WAIT_PRIVATE, ...) 系统调用休眠。 |
| 18 | + |
| 19 | +### runtime_Semrelease:协程唤醒与调度 |
| 20 | +```go |
| 21 | +// 释放信号量,唤醒一个或多个等待协程 |
| 22 | +// handoff 表示是否强制转移锁所有权(用于饥饿模式) |
| 23 | +// skipframes 表示堆栈跟踪跳过的帧数 |
| 24 | +func runtime_Semrelease(s *uint32, handoff bool, skipframes int) |
| 25 | +``` |
| 26 | +#### 核心逻辑 |
| 27 | +- 原子增加信号量:`atomic.Xadd(s, 1)` |
| 28 | +- 唤醒等待者:从队列中取出一个 sudog,调用操作系统原语解除阻塞,比如:linux futex(FUTEX_WAKE_PRIVATE, 1) 唤醒一个协程. |
| 29 | +- 调度转移 |
| 30 | + |
| 31 | +### sudog |
| 32 | +```go |
| 33 | +type sudog struct { |
| 34 | + g *g // 关联的协程 |
| 35 | + next *sudog // 队列中的下一个元素 |
| 36 | + prev *sudog // 队列中的上一个元素 |
| 37 | + elem unsafe.Pointer // 信号量指针 |
| 38 | + releasetime int64 // 协程被唤醒的时间戳 |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +## sync.Mutex |
| 43 | +### 通俗理解 |
| 44 | +- 把锁想象成公共厕所 |
| 45 | +- 假设有一个公共厕所(共享资源),很多人(goroutine)想用。但厕所一次只能进一个人。sync.Mutex 就是这个厕所的“管理员”,而 sema 是管理员手里的 “叫号器”。 |
| 46 | +- 叫号器(sema)的作用:不是直接管理厕所的门锁,而是管理 排队等待的人,不然会不断轮询检查锁状态。 |
| 47 | + |
| 48 | +## 一、sync.Mutex核心设计 |
| 49 | + |
| 50 | +```go |
| 51 | +type Mutex struct { |
| 52 | + state int32 // 状态字段 |
| 53 | + sema uint32 // 是一个信号量(semaphore),用于协程阻塞和唤醒。 |
| 54 | +} |
| 55 | + |
| 56 | +const ( |
| 57 | + mutexLocked = 1 << iota // 1 (二进制 001) |
| 58 | + mutexWoken // 2 (二进制 010) |
| 59 | + mutexStarving // 4 (二进制 100) |
| 60 | +) |
| 61 | +func (m *Mutex) Lock() { |
| 62 | + // 快速路径失败后进入慢路径 |
| 63 | + if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { |
| 64 | + return |
| 65 | + } |
| 66 | + runtime_SemacquireMutex(&m.sema, queueLifo, 1) |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +### sema 的核心作用 |
| 71 | +通过操作系统的 睡眠/唤醒原语(如 futex)管理协程的阻塞和恢复。 |
| 72 | +关键步骤: |
| 73 | +休眠:协程加入队列 → 调用 futex(FUTEX_WAIT) 进入内核态休眠。 |
| 74 | +唤醒:释放锁时调用 futex(FUTEX_WAKE) 通知队列中的协程。 |
| 75 | + |
| 76 | + |
| 77 | +1. **状态表示** |
| 78 | + - 32位 `state` 字段:低3位表示锁状态,高位记录等待的goroutine数量。 |
| 79 | + - 状态位: |
| 80 | + - `mutexLocked`(第0位):锁是否被持有 |
| 81 | + - `mutexWoken`(第1位):是否有唤醒的goroutine |
| 82 | + - `mutexStarving`(第2位):是否处于饥饿模式 |
| 83 | + |
| 84 | +2. **两种模式** |
| 85 | + | 模式 | 特点 | 触发条件 | |
| 86 | + | ------------ | ----------------------------------------------------- | -------------------- | |
| 87 | + | **正常模式** | 允许新goroutine插队竞争,支持自旋优化 | 默认模式 | |
| 88 | + | **饥饿模式** | 禁止插队,锁直接交给等待队列队首goroutine(完全公平) | 等待时间 >1ms 时触发 | |
| 89 | + |
| 90 | +## 二、关键流程 |
| 91 | +1. **加锁(Lock)** |
| 92 | + - **快速路径**:直接CAS抢锁(无竞争时) |
| 93 | + - **慢速路径**:自旋尝试 → 加入等待队列 → 根据模式竞争/获取锁 |
| 94 | + |
| 95 | +2. **解锁(Unlock)** |
| 96 | + - **快速路径**:无等待者时直接释放 |
| 97 | + - **慢速路径**:唤醒等待者,处理模式切换 |
| 98 | + |
| 99 | +## 三、性能优化 |
| 100 | +1. **自旋机制** |
| 101 | + - 触发条件:多核CPU + 非饥饿模式 + 短临界区 |
| 102 | + - 自旋次数:最多4次(约30个CPU时钟周期) |
| 103 | + |
| 104 | +2. **信号量控制** |
| 105 | + - 使用 `sema` 字段配合 `runtime_SemacquireMutex`/`runtime_Semrelease` 实现阻塞唤醒 |
| 106 | + |
| 107 | + |
| 108 | +## atomic |
| 109 | +原子操作的底层实现依赖于CPU提供的原子指令,不同架构的CPU有不同的实现方式。 |
0 commit comments