Skip to content

🐯 [并发] Release 在 Unlock 前不持锁 Broadcast 可能错过等待者 · credit.go #126

@github-actions

Description

@github-actions

来自 #125 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。

🛑 [阻塞 · 并发] Release 在 Unlock 前不持锁 Broadcast 可能错过等待者 pkg/credit/credit.go:60

问题根因Releasemu.Unlock() 之后才调用 cond.Broadcast()。Go 的 sync.Cond 语义要求 Broadcast 在锁内(或至少锁释放前)调用,否则存在竞态窗口:调用方 Acquire 可能恰好在这两个操作之间进入 cond.Wait(),从而错过本次 Broadcast,进而永远阻塞。具体时序:

  1. 线程 A(Release):mu.Unlock() → 锁已释放
  2. 线程 B(Acquire):进入 Wait(),释放锁、挂起
  3. 线程 A:cond.Broadcast() → 此时 B 已挂起,但 A 的 Broadcast 已发出,B 错过信号
  4. B 永久饥饿

为什么低级解法不够:通用评审者会简单互换两行(先 Broadcast 再 Unlock),但这仍需检查 Go 官方 sync.Cond 文档中 "Broadcast 可以在锁内或锁外安全调用,但通知可能丢失" 的常见陷阱,且这个问题的根因是 Cond 使用模式的不正确——需要理解 Cond 的 Wait 语义。不过此处最简单的修法(keeping lock for broadcast)就是正确的修法,因为 Broadcast 在锁内调用完全合法。

架构级方案:将 ReleaseBroadcast 移到 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 使用模式,不改变任何语义和性能。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions