
sync.cond 用于让多个 goroutine 在共享条件满足前安全阻塞,其核心是“先加锁、再检查条件、不满足则 wait(自动释放锁并挂起)、被唤醒后重新加锁并再次检查”,必须严格遵循此循环模式,否则将导致死锁或竞态。
sync.cond 用于让多个 goroutine 在共享条件满足前安全阻塞,其核心是“先加锁、再检查条件、不满足则 wait(自动释放锁并挂起)、被唤醒后重新加锁并再次检查”,必须严格遵循此循环模式,否则将导致死锁或竞态。
sync.Cond 是 Go 标准库中用于多生产者-多消费者场景下协调共享状态变化的重要同步原语。它本身不提供互斥保护,而是依赖传入的 sync.Locker(通常是 *sync.Mutex 或 *sync.RWMutex)来保障对共享数据和条件判断的线程安全。常见误区(如问题中所示)是忽略 Wait() 的前置条件检查与循环等待逻辑,从而引发死锁。
✅ 正确用法:三步循环模式
每个等待方必须严格遵循以下结构:
cond.L.Lock()
for !conditionMet() { // 必须用 for 而非 if!防止虚假唤醒
cond.Wait() // 自动释放锁 → 挂起 → 被唤醒时重新获取锁
}
// 此时 conditionMet() 为 true,且 cond.L 已锁定
// 执行业务逻辑(访问共享资源)
cond.L.Unlock()
关键点解析:
- for 循环不可省略:Wait() 可能因信号中断或调度器原因被虚假唤醒(spurious wakeup),仅用 if 会导致逻辑错误或永久阻塞。
- 锁必须由调用方显式管理:Wait() 内部会自动解锁并休眠;唤醒后自动重新加锁,因此 Lock()/Unlock() 必须成对出现在 Wait() 前后作用域。
-
通知方需在持有锁时修改状态并通知:
cond.L.Lock() sharedData = newValue cond.Broadcast() // 或 cond.Signal() cond.L.Unlock()
? 实际示例:等待 HTTP 响应头就绪
适配提问者的使用场景——多个 goroutine 等待下载器写入 headers:
package main
import (
"fmt"
"sync"
"time"
)
var (
headers map[string][]string
headerCond = sync.NewCond(&sync.Mutex{})
)
func downloader() {
time.Sleep(2 * time.Second) // 模拟下载耗时
headerCond.L.Lock()
headers = map[string][]string{
"Content-Type": {"application/octet-stream"},
"Content-Length": {"10485760"},
}
fmt.Println("✅ Headers ready, broadcasting...")
headerCond.Broadcast() // 通知所有等待者
headerCond.L.Unlock()
}
func waitForHeaders(id int) {
headerCond.L.Lock()
defer headerCond.L.Unlock()
// 关键:循环检查,避免虚假唤醒
for headers == nil {
fmt.Printf("⏳ Goroutine %d waiting for headers...\n", id)
headerCond.Wait()
}
fmt.Printf("? Goroutine %d got headers: %+v\n", id, headers)
}
func main() {
var wg sync.WaitGroup
// 启动多个等待者
for i := 1; i <blockquote><p>✅ 输出示例(顺序可能略有差异):<br>⏳ Goroutine 1 waiting for headers...<br>⏳ Goroutine 2 waiting for headers...<br>✅ Headers ready, broadcasting...<br>? Goroutine 1 got headers: map[Content-Length:[10485760] Content-Type:[application/octet-stream]]<br>? Goroutine 2 got headers: ... </p></blockquote><h3>⚠️ 注意事项与替代建议</h3>
- 不要单独使用 sync.Cond:它不解决数据竞争,仅解决“等待-通知”协作。所有对共享变量(如 headers)的读写仍需通过 cond.L 保护。
- 优先考虑 Channel:若场景符合“一次写、多次读”,可结合 sync.Once + chan struct{} 或 sync.Map + channel 广播;若需历史值复用,sync.Cond 更合适。
- 避免 Broadcast 性能开销:当只需唤醒一个等待者时,用 Signal() 替代 Broadcast()。
- 超时处理:Wait() 不支持超时,如需防止单点故障,可改用 select + time.After 配合 Cond,或直接选用 context.WithTimeout + channel 方案。
总之,sync.Cond 是一把精准但需要谨慎使用的“手术刀”——它解决的是条件驱动的协作等待,而非通用同步。只要牢记“锁→查→等→再查”循环范式,并始终用 for 包裹条件判断,就能彻底规避死锁与竞态风险。











