Go 标准库无内置 sync.Barrier,需用 sync.WaitGroup(预设计数)、sync.Mutex+sync.Cond(支持复用)或 channel(需严格配对收发)实现;真实场景中多被 WaitGroup 或 context 模式替代。

Go 里没有内置 Barrier,得自己造
Go 标准库确实没提供 sync.Barrier 这种类型。你搜 sync.Barrier 会 404,不是你环境坏了,是它压根不存在。想让 N 个 goroutine 在某个点齐步走,得靠 sync.WaitGroup + sync.Mutex 或 sync.Cond 组合实现,或者用 channel 配合计数器。别试图 import 一个不存在的包。
用 sync.WaitGroup 模拟 Barrier 最轻量
这是最常用、也最容易理解的做法:每个 goroutine 到达栅栏时调用 wg.Add(1)(注意不是初始化时加),然后 wg.Wait() 等待全部到达;等所有都到了,再统一 wg.Done() 清零。但直接这么写会死锁 —— 因为 wg.Wait() 必须在所有 Add 之后调用,而 goroutine 是并发的。
正确做法是预设参与数,用两次 WaitGroup:
- 第一次
wg1用于“登记到达”,每个 goroutine 执行wg1.Done()表示我到了 - 主线程等
wg1.Wait()完成后,再用wg2触发下一轮(比如广播信号) - 或者更干脆:用一个
sync.Mutex+ 计数器 +sync.Cond,避免多次 WaitGroup 嵌套
简单示意(无锁版,适合固定 N):
立即学习“go语言免费学习笔记(深入)”;
var (
mu sync.Mutex
count int
cond *sync.Cond
)
func init() {
cond = sync.NewCond(&mu)
}
func barrier(n int) {
mu.Lock()
count++
if count == n {
cond.Broadcast() // 全齐了,喊所有人继续
count = 0 // 重置,支持复用
} else {
cond.Wait() // 没齐,先睡
}
mu.Unlock()
}
channel 实现 Barrier 容易卡死
有人喜欢用带缓冲 channel 当信号灯:开一个 make(chan struct{}, N),每个 goroutine 到达时塞一个 struct{},等第 N 个塞完,再从 channel 里读 N 次清空它。问题在于:如果某个 goroutine panic 或提前退出,channel 就永远塞不满,其余 goroutine 全卡在 ch <- struct{}{} 上。
更糟的是,如果多个栅栏连续用,没清空 channel 就进下一轮,行为完全不可控。
- 必须确保所有参与者严格执行“发送 + 接收”配对
- 不能依赖 defer 清理,因为 defer 在 panic 后才跑,来不及救场
- 缓冲大小必须等于预期 goroutine 数,错一个就阻塞
- 性能上比
sync.Cond略差,毕竟涉及 goroutine 调度和 channel runtime 开销
真实场景中,Barrier 很少单独存在
你几乎不会为了“齐步走”而专门写个栅栏。它通常藏在更上层的模式里:
- 批处理任务分片后并行计算,最后汇总 —— 这时用
sync.WaitGroup更自然 - 测试里要等所有 goroutine 启动完毕再开始打点 —— 可用
sync.Once+ channel 通知 - 热重启时等待旧请求处理完再关服务 —— 这是
context.WithTimeout+WaitGroup的组合
硬套 Barrier 模型反而增加复杂度。Go 的哲学是“用 channel 和 goroutine 组合表达意图”,而不是复刻 Java/C++ 里的同步原语。
真正容易被忽略的点是:栅栏一旦复用,就必须考虑重入安全和状态清理。比如 sync.Cond 的 Wait() 返回后,条件未必仍成立(spurious wakeup),得在外层加 for 循环检查;而 WaitGroup 的 Add() 不能在 Wait() 后调用,否则 panic。这些边界,不跑压测根本看不出问题。










