来自 #125 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
🛑 [阻塞 · 并发] Release 在 Unlock 前不持锁 Broadcast 可能错过等待者 pkg/credit/credit.go:60
问题根因:Release 在 mu.Unlock() 之后才调用 cond.Broadcast()。Go 的 sync.Cond 语义要求 Broadcast 在锁内(或至少锁释放前)调用,否则存在竞态窗口:调用方 Acquire 可能恰好在这两个操作之间进入 cond.Wait(),从而错过本次 Broadcast,进而永远阻塞。具体时序:
- 线程 A(Release):
mu.Unlock() → 锁已释放
- 线程 B(Acquire):进入
Wait(),释放锁、挂起
- 线程 A:
cond.Broadcast() → 此时 B 已挂起,但 A 的 Broadcast 已发出,B 错过信号
- B 永久饥饿
为什么低级解法不够:通用评审者会简单互换两行(先 Broadcast 再 Unlock),但这仍需检查 Go 官方 sync.Cond 文档中 "Broadcast 可以在锁内或锁外安全调用,但通知可能丢失" 的常见陷阱,且这个问题的根因是 Cond 使用模式的不正确——需要理解 Cond 的 Wait 语义。不过此处最简单的修法(keeping lock for broadcast)就是正确的修法,因为 Broadcast 在锁内调用完全合法。
架构级方案:将 Release 中 Broadcast 移到 mu.Unlock() 之前。标准 Cond 用法模式:
func (p *Pool) Release(n int64) {
p.mu.Lock()
p.used -= n
if p.used < 0 {
p.used = 0
}
p.cond.Broadcast() // 在锁内 Broadcast,保证等待者不会错过
p.mu.Unlock()
}
这是 Go 官方 FAQ 推荐的 sync.Cond 使用模式。Broadcast 在锁内调用保证:在 Broadcast 之后、Unlock 之前,任何新进入 Wait 的 goroutine 都能看到 used 已更新且不会真正阻塞(因为 fits 会返回 true)。
代价/收益:零代价。仅仅是调整两行顺序。正确的 Cond 使用模式,不改变任何语义和性能。
🛑 [阻塞 · 并发] Release 在 Unlock 前不持锁 Broadcast 可能错过等待者
pkg/credit/credit.go:60问题根因:
Release在mu.Unlock()之后才调用cond.Broadcast()。Go 的sync.Cond语义要求Broadcast在锁内(或至少锁释放前)调用,否则存在竞态窗口:调用方Acquire可能恰好在这两个操作之间进入cond.Wait(),从而错过本次Broadcast,进而永远阻塞。具体时序:mu.Unlock()→ 锁已释放Wait(),释放锁、挂起cond.Broadcast()→ 此时 B 已挂起,但 A 的 Broadcast 已发出,B 错过信号为什么低级解法不够:通用评审者会简单互换两行(先 Broadcast 再 Unlock),但这仍需检查 Go 官方
sync.Cond文档中 "Broadcast 可以在锁内或锁外安全调用,但通知可能丢失" 的常见陷阱,且这个问题的根因是 Cond 使用模式的不正确——需要理解 Cond 的 Wait 语义。不过此处最简单的修法(keeping lock for broadcast)就是正确的修法,因为Broadcast在锁内调用完全合法。架构级方案:将
Release中Broadcast移到mu.Unlock()之前。标准 Cond 用法模式:这是 Go 官方 FAQ 推荐的
sync.Cond使用模式。Broadcast在锁内调用保证:在Broadcast之后、Unlock之前,任何新进入Wait的 goroutine 都能看到used已更新且不会真正阻塞(因为fits会返回 true)。代价/收益:零代价。仅仅是调整两行顺序。正确的 Cond 使用模式,不改变任何语义和性能。