sync.mutex 解锁后不一定立刻唤醒等待协程,因其默认正常模式允许新协程插队抢锁;仅当等待超1毫秒才切饥饿模式,确保fifo唤醒。rwmutex通过readercount和readerwait协同防写饥饿,漏调runlock会导致永久阻塞。

为什么 sync.Mutex 解锁后不一定立刻唤醒等待协程
因为 sync.Mutex 默认启用「正常模式」:新来的协程有更高概率抢到刚释放的锁,而不是交给队列头的等待者。这会导致等待协程被「插队」,尤其在高并发写场景下,个别协程可能长期得不到锁——不是 bug,是设计权衡。
只有当某个协程等待超过 starvationThresholdNs(默认 1 毫秒)时,Mutex 才自动切换到「饥饿模式」,此时解锁会直接把锁交给队列最前面的等待者,禁止新协程插队。
-
state字段的mutexStarving位(值为 4)被置位后,就进入饥饿模式 - 饥饿模式下,所有新争抢者都会被强制加入队尾,不再自旋
- 一旦队列清空,会自动切回正常模式
sync.RWMutex 的 readerCount 和 readerWait 怎么协同防写饥饿
RWMutex 内部用 readerCount 记当前持有读锁的协程数,用 readerWait 记「在写锁请求发出后、尚未完成读锁释放」的协程数量。这两个字段共同构成写操作的等待条件。
当协程调用 Lock() 时,它先原子递减 readerCount,然后检查是否归零;若未归零,就阻塞在 writerSem 上,直到所有已持有的读锁全部释放完毕(即 readerWait 归零)。
- 每次
RLock()会增加readerCount,但仅当readerWait > 0时,才额外递增readerWait - 每次
RUnlock()先减readerCount,再检查是否触发写锁唤醒:若readerCount == 0 && readerWait > 0,则释放writerSem - 这就是为什么「持续读」会阻塞写,但写不会无限等待——只要读锁全部释放,写就能立刻上
读锁不加 defer 容易导致写饥饿,但写锁加了也不一定安全
RLock() / RUnlock() 必须严格配对,漏掉一次 RUnlock() 会让 readerCount 永远不归零,后续所有 Lock() 都永久阻塞——这是典型的资源泄漏,比互斥锁死锁更隐蔽。
而写锁虽然也建议用 defer mu.Unlock(),但要注意:如果在 Lock() 后、defer 前发生 panic,Unlock() 仍会被执行;但如果 Lock() 自身 panic(比如因内存不足),那根本没机会注册 defer,锁就永远卡住。
- 永远不要在
RLock()后直接 return,必须确保RUnlock()执行 - 避免在持有读锁期间调用可能 panic 的函数,或用
recover包裹 -
RWMutex不提供类似TryRLock的非阻塞接口,无法优雅降级
底层信号量 writerSem 和 readerSem 是系统级资源
RWMutex 内部的 writerSem 和 readerSem 是 uint32 类型的信号量,由运行时调用 runtime_Semacquire / runtime_Semrelease 管理,它们绑定的是 OS 级线程等待队列,不是 Go 协程调度器的本地队列。
这意味着:一旦协程因获取读/写锁失败而休眠,它就会脱离 M-P-G 调度体系,进入操作系统内核等待——唤醒开销比纯协程调度更大,且无法被 runtime.Gosched() 影响。
- 频繁争抢读锁(尤其在高并发只读场景)可能导致大量系统调用,压垮内核调度器
- 如果读锁持有时间过长(比如含网络 I/O 或大循环),会拖慢整个写锁路径
- 没有配置项能限制最大等待协程数,失控时可能耗尽文件描述符或线程栈
readerCount 怎么变、readerWait 何时动、state 里哪个 bit 控制饥饿,比背诵 API 更关键——因为 panic 发生时,文档不会帮你修 bug,但你知道信号量在哪、谁在等、为什么等不到。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











