Go 语言 sync.Mutex 与 RWMutex 源码解析
本文档旨在通过剖析源代码,总结 Go 语言标准库中 Mutex 和 RWMutex 的工作原理。内容涵盖互斥锁的快速/慢速处理流程、自旋与信号量切换机制,以及读写锁的读者-_writer_计数协调策略。
Mutex (互斥锁) 内部机制
sync.Mutex 是 Go 中最基础的同步原语。其设计核心在于兼顾无竞争时的低开销(CAS)与高竞争时的公平性(饥饿模式)。
Lock() 快速路径:原子操作上锁
当 Mutex 未被锁定且无等待者时,通过 atomic.CompareAndSwapInt32 将状态从 0 变为 mutexLocked。这是最快的路径,直接返回。
// Lock locks m.
func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
m.lockSlow()
}
lockSlow():阻塞、自旋与状态管理
当快速路径失败时,进入 lockSlow 循环。该函数负责处理自旋等待、入队以及饥饿模式的触发。
- 自旋等待:如果当前处于正常模式且存在其他 Goroutine 在等待(waiters > 0),且锁已被占用,代码尝试进行短暂自旋(
runtime_doSpin),在此期间标记mutexWoken以避免后续解锁唤醒多余的 Goroutine。if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // ...设置woken标志... runtime_doSpin() iter++ old = m.state continue } - CAS 更新状态:计算期望状态
new:
- 若非饥饿态且未加锁,加入mutexLocked。
- 若被锁或饥饿,增加 Waiter 数量(左移mutexWaiterShift)。
- 若此前已自旋唤醒过 (awoke),清除该标志。 - 阻塞入队:若 CAS 修改成功但此时锁仍被占用,说明当前 Goroutine 需要等待。调用
runtime_SemacquireMutex进行阻塞。此过程支持 LIFO(后进先出)排队以提升局部性,但在饥饿模式下强制 FIFO。 - 饥饿模式判定:Goroutine 被唤醒后,若等待时间超过阈值(默认 1ms)或原本就是饥饿模式,则进入/维持饥饿状态。在此模式下,锁所有权由解锁者直接“交接”给队头等待者,而非随机唤醒,确保先来后到的公平性。
Unlock() 释放锁与交接
释放锁分为两步:
- 快速路径:使用
atomic.AddInt32减去mutexLocked。如果返回值不为 0,说明有等待者或有状态残留,进入慢速路径。 - UnlockSlow() 交接:
- 普通模式:若没有 waiter 或已有 Goroutine 被唤醒,直接退出。否则,将mutexWoken置位并减少 waiter 数,通过信号量唤醒下一个等待者。
- 饥饿模式:直接调用runtime_Semrelease(..., handoff=true)。参数handoff=true意味着直接将控制权移交给下一个 waiter,令其立即执行,不再进入 P 的运行队列等待调度,从而大幅降低饥饿状态的延迟。
RWMutex (读写锁) 实现细节
RWMutex 允许多个读协程同时运行,但写协程独占资源。其状态管理依赖于整数的不同比特位。
数据结构核心字段
type RWMutex struct {
w Mutex // 辅助写锁,用于串行化写的请求
writerSem uint32 // 写者的信号量,等待活跃读者完成
readerSem uint32 // 读者的信号量,等待写者完成
readerCount int32 // 活跃读者数量(负数表示有写者等待)
readerWait int32 // 等待写者完成的读者数量
}
关键位运算技巧:
- readerCount 的最高位被用作“写锁占位符”。
- 常量 rwmutexMaxReaders = 1 << 30。
- 当写入 -rwmutexMaxReaders 时,会将最高位置 1(负数),且剩余位全为 0,标志着有一个写者正在获取锁。
写锁获取 (Lock)
- 排除竞争:首先获取内部的
w.Lock(),确保同一时刻只有一个写协程在进行 Lock 的逻辑处理。 - 通知读者:通过
atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)将计数器转为负数,并向所有正在执行的读者广播“有写者在排队”。 - 等待读者完成:将当前
readerCount的数量转移至readerWait。如果还有活跃读者,阻塞在writerSem上,直到所有读者执行完毕并发送信号。
写锁释放 (Unlock)
通过加法将 readerCount 恢复为正数,解除对读者的拦截。随后循环唤醒所有等待在 readerSem 上的读协程,最后释放内部的 w.Unlock(),允许其他写者继续尝试。
读锁获取 (RLock)
- 原子递增:执行
AddInt32(&rw.readerCount, 1)。 - 冲突检测:检查结果是否为负数。如果是负数,说明有写者正在等待或持有锁。此时当前读协程将被挂起在
readerSem信号量上,直到写者释放锁。
读锁释放 (RUnlock)
原子递减 readerCount。若结果为负数,说明之前有过写者等待。若该协程是最后一个活跃的读者(readerWait 减至 0),则释放 writerSem 信号量,唤醒正在等待的写协程。
以上就是Go语言互斥锁与读写锁内部实现的详细内容,更多关于Go并发编程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













