Mutex 实现原理
🔴 困难题目描述
解释 Go 的 sync.Mutex 的底层实现原理。为什么 Mutex 不能复制?
示例代码
type Counter struct {
mu sync.Mutex
count int
}
// ❌ 错误:Mutex 被复制
func (c Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// ✅ 正确:使用指针接收者
func (c *Counter) Increment() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}提示
- Mutex 包含状态字段
- 复制后状态不一致
- 可能导致死锁或数据竞争
解法
参考答案 (3 个标签)
Mutex 锁 底层实现
底层结构
type Mutex struct {
state int32 // 状态位
sema uint32 // 信号量
}状态位解析
state 的 32 位:
┌────────────────────────────────────────┐
│ 31-3位 │ 2 │ 1 │ 0 │
│ waiter(等待者数量) │W │L │L │
│ │ │o │o │
│ │ │c │c │
│ │ │k │k │
│ │ │e │e │
│ │ │d │d │
└────────────────────────────────────────┘
- locked (bit 0): 是否锁定
- woken (bit 1): 是否有唤醒的 goroutine
- waiter (bit 2-31): 等待的 goroutine 数量锁的两种模式
正常模式(Normal Mode)
func (m *Mutex) Lock() {
// 快速路径:无竞争
if atomic.CompareAndSwapInt32(&m.state, 0, locked) {
return
}
// 慢速路径:有竞争
m.lockSlow()
}
func (m *Mutex) lockSlow() {
for {
// 1. 尝试自旋(多核 CPU)
for i := 0; i < spinMax; i++ {
if atomic.CompareAndSwapInt32(&m.state, 0, locked) {
return
}
procyield(30) // 让出 CPU
}
// 2. 设置 waiter 标志
old := atomic.AddInt32(&m.state, locked|waiterShift)
// 3. 休眠
runtime_SemacquireMutex(&m.sema)
// 4. 被唤醒后,与其他 goroutine 竞争
if atomic.CompareAndSwapInt32(&m.state, old, locked) {
return
}
}
}特点:
- 新来的 goroutine 与被唤醒的 goroutine 竞争
- 新来的更容易成功(还在 CPU 上运行)
- 可能导致”饥饿”,但吞吐量高
饥饿模式(Starvation Mode)
func (m *Mutex) lockSlow() {
starvation := false
for {
// 检查是否进入饥饿模式
// 等待时间 > 1ms
if runtime_nanotime()-m.waitStartTime > starvationThresholdNs {
atomic.AddInt32(&m.state, starvationFlag)
starvation = true
}
// 饥饿模式:直接交给队头
if starvation && (old&locked == 0) {
atomic.AddInt32(&m.state, -waiterShift)
return
}
runtime_SemacquireMutex(&m.sema)
}
}触发条件:
- 等待时间超过 1ms
- goroutine 饥饿
退出条件:
- 等待者只剩 1 个
为什么不能复制
type Counter struct {
mu sync.Mutex
count int
}
// 值接收者:复制了 Mutex
func (c Counter) Increment() {
c.mu.Lock() // 复制后的 Mutex,state = 0
c.count++
c.mu.Unlock() // 释放的是复制的 Mutex
}
// 问题:
// 1. 复制后 state 被重置
// 2. 原始 Mutex 可能被锁定
// 3. 状态不一致,导致死锁示例:
func main() {
counter := Counter{count: 0}
// goroutine 1
go func() {
counter.mu.Lock()
time.Sleep(time.Second)
counter.mu.Unlock()
}()
time.Sleep(100 * time.Millisecond)
// goroutine 2:复制了 Mutex
counter.Increment() // ❌ 可能死锁
}noCopy 机制
type Mutex struct {
state int32
sema uint32
// noCopy 字段:防止复制
noCopy noCopy
}
type noCopy struct{}
// go vet 会检测到复制
func (*noCopy) Locker() {}扩展:RWMutex
底层结构
type RWMutex struct {
w Mutex // 写锁
writerSem uint32 // 写者等待信号量
readerSem uint32 // 读者等待信号量
readerCount int32 // 读者数量
readerWait int32 // 写者等待时,读者数量
}读写锁规则
func (rw *RWMutex) RLock() {
// readerCount < 0:有写者等待
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 等待写者完成
runtime_SemacquireMutex(&rw.readerSem)
}
}
func (rw *RWMutex) Lock() {
// 获取写锁
rw.w.Lock()
// 阻止新读者
r := atomic.AddInt32(&rw.readerCount, -rwmaxReaders) + rwmaxReaders
if r != 0 {
// 等待现有读者完成
atomic.AddInt32(&rw.readerWait, r)
runtime_SemacquireMutex(&rw.writerSem)
}
}RWMutex 问题
// ❌ 不能嵌套
func nested() {
rw.RLock()
rw.RLock() // 可重入
rw.Lock() // ❌ 死锁!
rw.Unlock()
rw.RUnlock()
rw.RUnlock()
}
// ❌ 读锁升级为写锁会死锁
func upgrade() {
rw.RLock()
// ...
rw.Lock() // ❌ 死锁!
rw.Unlock()
rw.RUnlock()
}