sync.WaitGroup 不是屏障,仅适用于等待所有 goroutine 完成;真屏障需用 sync.Cond 实现,核心是 mutex 保护计数器+Broadcast 唤醒全部 goroutine。

用 sync.WaitGroup 做简单屏障,但别误当“等待所有 goroutine 到达某点”
sync.WaitGroup 常被拿来凑合当屏障——先 Add(n),每个 goroutine 结束时调 Done(),主 goroutine 调 Wait() 等全部完成。但它不是真正的屏障:它只等“结束”,不保证“同时到达某执行点”。比如你希望 5 个 goroutine 都打印完 "ready" 后才一起执行下一步,WaitGroup 做不到——它不管你在哪停,只认谁 Done() 了。
常见错误现象:WaitGroup 等到了,但部分 goroutine 还卡在 fmt.Println("ready") 前面,导致后续逻辑错序;或者因漏调 Done() 死锁。
- 仅适用于“等全部任务完成”的场景,比如启动一批 worker 处理完各自数据后收尾
- 不能用于“所有 goroutine 必须在某个代码行之前暂停,再集体往下走”
- 记得在 goroutine 内部用
defer wg.Done(),避免 panic 导致漏调
真屏障得用 sync.Barrier?不,Go 标准库没有这个类型
别搜 sync.Barrier——它不存在。Go 官方没提供原生屏障(barrier)类型,这是和 Java 的 CyclicBarrier 或 Python 的 threading.Barrier 最大区别。有人翻源码或查文档白忙一场,就是栽在这儿。
替代方案只有两个靠谱方向:自己基于 sync.Mutex + sync.Cond 实现,或者用 sync/atomic 配合循环检测(轻量但 busy-wait 不优雅)。前者是标准解法,后者只适合极简、低频、对延迟极度敏感的场景。
立即学习“go语言免费学习笔记(深入)”;
-
sync.Cond是唯一能安全挂起多个 goroutine 并统一唤醒的机制 - 必须配
sync.Mutex使用,Cond.L就是它的锁,不能传错 - 唤醒要用
.Broadcast(),不是Signal()——后者只醒一个,不满足“全同步”要求
手写屏障:用 sync.Cond 实现可重用的 Barrier
核心逻辑就三步:计数器归零前阻塞所有 goroutine;最后一个到达者把计数器重置并广播;其他 goroutine 被唤醒后继续。注意“重置”必须在锁内完成,否则有竞态。
type Barrier struct {
mu sync.Mutex
cond *sync.Cond
waiting int
total int
}
func NewBarrier(n int) *Barrier {
b := &Barrier{total: n}
b.cond = sync.NewCond(&b.mu)
return b
}
func (b *Barrier) Wait() {
b.mu.Lock()
defer b.mu.Unlock()
b.waiting++
if b.waiting == b.total {
b.waiting = 0
b.cond.Broadcast()
} else {
b.cond.Wait()
}
}
使用场景:比如启动 4 个 goroutine 加载配置,都打印 "loaded" 后再一起初始化服务;或者压力测试中让所有 worker 在同一毫秒开始发请求。
- 每次
Wait()都会阻塞,直到第total个调用者抵达并重置计数器 - 参数
n是固定人数,不能动态变;如果要支持重用且人数可变,得加额外字段和校验 - 别在
Wait()里做耗时操作,否则拖慢整个屏障释放——它本质是串行临界区
容易踩的坑:cond.Wait() 自动释放锁,但唤醒后要重新抢锁
这是最常翻车的地方。cond.Wait() 会原子性地释放关联的 mu 并挂起 goroutine;被 Broadcast() 唤醒后,它会自动重新获取 mu 才返回。所以你的临界区逻辑(比如检查计数器、修改状态)必须严格包在 Lock()/Unlock() 之间,且不能假设唤醒瞬间状态还有效。
典型错误:在 cond.Wait() 返回后直接读 b.waiting,以为还是旧值——其实可能已被别的 goroutine 先抢到锁改掉了。
- 所有对共享状态的读写,包括唤醒后的判断,都必须在
mu.Lock()之后 - 不要在
Wait()外层加额外锁,会导致死锁——cond.Wait()内部已经管锁了 - 如果 barrier 要求“每轮只允许一组 goroutine 通过”,就得加轮次号(
epoch)字段防重入,否则上一轮还没播完,下一轮就进来了
真正麻烦的从来不是写出来,而是想清楚“谁在什么时候持有哪把锁”“状态变更是否可见”“唤醒时机和顺序是否可控”。这些细节一错,程序就变成随机行为。


















