Go 语言实现——同步原语 ========================= Mutex ````````` 正常模式 ------------ 先看 Go1.8 的 Mutex 实现,这个版本的 Mutex 实现还比较简单,1.9 开始 Mutex 加入了一个饥饿模式的优化,这个后面再说。 Mutex 定义如下: .. code-block:: go type Mutex struct { state int32 sema uint32 } 其中 ``state`` 为当前 Mutex 的状态,``sema`` 是解锁信号量。 ``state`` 的状态位定义如下: .. code-block:: go const ( mutexLocked = 1 << iota // mutex is locked mutexWoken mutexWaiterShift = iota ) 从最低位开始: .. image:: images/go-mutex-state.png - 第一个比特位表示“当前 Mutex 是不是已经上锁”。 - 第二个比特位表示“当前有 goroutine 处于自旋或者收到解锁信号目前处于运行状态”。 - 剩余的位用作计数器,存当前有多少 goroutine 正在等待解锁的信号量。 Mutex 上锁的逻辑如下: 1. 首先使用 CAS 尝试上锁,如果成功直接返回。 2. 如果失败,自旋几次等待解锁并重新尝试上锁。 3. 自旋次数太多后,将 goroutine 睡眠,等待 Unlock 发信号唤醒。 .. code-block:: go func (m *Mutex) Lock() { // CAS 尝试上锁,成功直接返回 if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { return } awoke := false iter := 0 for { old := m.state new := old | mutexLocked // 如果锁已经被其它 goroutine 持有了 if old&mutexLocked != 0 { // 检查当前 goroutine 能不能够自旋 if runtime_canSpin(iter) { // 设置 woken 标示位,告诉 Unlock 不用唤醒 goroutine // 有 goroutine 处在运行状态 if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } // 一次自旋等待解锁 runtime_doSpin() iter++ continue } // 不能自旋,计数器 + 1 new = old + 1<= 4 || ncpu <= 1 || int32(sched.npidle+sched.nmspinning)+1 >= gomaxprocs { return false } if p := getg().m.p.ptr(); !runqempty(p) { return false } return true } 自旋就是执行 30 次 PAUSE 指令。 .. code-block:: go func sync_runtime_doSpin() { procyield(30) } TEXT runtime·procyield(SB),NOSPLIT,$0-0 MOVL cycles+0(FP), AX again: PAUSE SUBL $1, AX JNZ again RET 而解锁的逻辑就是: 1. 首先解锁。如果等待锁的 goroutine 有在运行状态的,直接返回就行。 2. 如果没有,那么使用信号量给等待的 goroutine 发送个信号。 .. code-block:: go func (m *Mutex) Unlock() { // 解锁 new := atomic.AddInt32(&m.state, -mutexLocked) old := new for { // old>>mutexWaiterShift 是当前等待解锁信号量的 goroutine 计数器 // 如果没有等待解锁信号量的 goroutine,或者刚解的锁已经被其它 goroutine 重新上锁 // 或者有在自旋等待锁的 goroutine,直接返回。 if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken) != 0 { return } // 等待解锁信号量的 goroutine 数减 1 并且设置“已经有 goroutine 唤醒”标志位。 new = (old - 1<