Go标准库未提供sync.Barrier,需用sync.WaitGroup与chan struct{}组合实现可重用栅栏:通过原子计数、单次close信号和及时重建channel确保线程安全与多次等待。

Go 里没有内置 sync.Barrier,得自己造
Go 标准库确实没提供 Barrier 类型——不像 Java 的 CyclicBarrier 或 Python 的 threading.Barrier。这不是遗漏,是设计取舍:Go 倾向用 channel + sync.WaitGroup 组合表达同步意图,而非抽象出一个“等所有人到齐再一起走”的通用栅栏。
自己实现时,核心逻辑就三步:计数、阻塞、重置。但直接用 sync.Mutex + sync.Cond 容易写错唤醒逻辑,最稳妥的方式是复用 sync.WaitGroup 的等待能力,配合一个原子计数器和信号 channel。
-
sync.WaitGroup负责“等待全部到达”,但它不支持重入;所以每次栅栏触发后必须重置 - 不能用
time.Sleep轮询判断,会浪费 CPU 且不准 - 多个协程同时调用
Wait()时,必须确保只有一个能执行“广播”动作,否则会重复发信号
用 sync.WaitGroup + chan struct{} 实现可重用栅栏
下面这个实现能安全支持多次等待,且无竞态:
type Barrier struct {
wg sync.WaitGroup
mu sync.Mutex
count int
ready chan struct{}
total int
}
<p>func NewBarrier(n int) *Barrier {
return &Barrier{
total: n,
ready: make(chan struct{}),
}
}</p><p>func (b *Barrier) Wait() {
b.mu.Lock()
b.count++
if b.count == b.total {
// 最后一个到达者负责广播
close(b.ready)
b.ready = make(chan struct{}) // 重置
b.count = 0
}
b.mu.Unlock()</p><pre class="brush:php;toolbar:false;"><code><-b.ready // 等待广播}
立即学习“go语言免费学习笔记(深入)”;
关键点:
-
close(b.ready)是唯一能唤醒所有等待者的操作,且只由最后一个协程执行 -
b.ready = make(chan struct{})必须在 close 后立即重建,否则下一轮会 panic(向已关闭 channel 接收是允许的,但会立刻返回零值;而我们需要的是阻塞) - 别把
sync.WaitGroup当计数器用——它不支持 Get(),也不适合做条件判断,这里只借它的语义“等待完成”,实际计数靠b.count
为什么不用 sync.Cond?容易掉进唤醒丢失坑
常见错误写法是用 sync.Cond + cond.Wait() 等待,然后用 cond.Broadcast() 唤醒。问题在于:cond.Wait() 是先解锁再挂起,中间存在时间窗口——如果 Broadcast() 在某个 goroutine 还没进入 Wait() 就发生了,那个 goroutine 就永远卡住。
- 这种唤醒丢失(missed wakeup)在高并发或调度延迟时极易复现,而且难以调试
-
sync.Cond要求调用Wait()前必须持有锁,而锁的粒度控制稍有不慎就会引发死锁或性能瓶颈 - channel 的
操作天然具备“发送即唤醒”语义,且 Go runtime 保证了其原子性,比手写 cond 更可靠
实际使用中要注意协程数量和重置时机
Barrier 不是万能同步原语,它隐含一个强假设:每次等待的协程数量严格等于初始化时传入的 n。少一个,全员阻塞;多一个,panic 或行为未定义。
- 不要在 for 循环里动态增减参与协程数——Barrier 不是为这种场景设计的
- 如果某次等待后部分协程退出、新协程加入,必须确保下一次
Wait()前所有参与者都已就位,否则会卡死 - 在测试中模拟超时等待?别改 Barrier 本身,应该在外层加
select+time.After,比如:select { case
真正难的不是写对一次 Wait,而是让所有协程对“谁该来、什么时候来、来几次”达成一致。这往往得靠上层协议约束,而不是靠 Barrier 自己解决。










