Barrier是C#中支持多轮复用的汇合同步原语,适用于多个线程必须全部到达某点后才共同继续执行的场景;它自动进入下一轮等待,支持回调、阶段编号,但不支持取消或中断,需自行包装以实现超时和异常处理。

Barrier 是什么,什么时候该用它Barrier 是 C# 中用于多线程“汇合同步”的轻量级原语,适用于多个线程必须**全部到达某个逻辑点后才一起继续执行**的场景。它不像 ManualResetEvent 那样需要手动计数和重置,也不像 CountdownEvent 那样只做一次性等待——Barrier 支持**多次复用**,每次汇合后自动进入下一轮。
常见适用场景包括:并行计算的迭代步进(如数值模拟每轮更新)、分段处理后统一汇总、测试中模拟多线程竞态时的可控停靠点。
初始化 Barrier 并让线程等待
创建时需指定参与线程总数,这个数在生命周期内不可变:
var barrier = new Barrier(4); // 期待 4 个线程到达
每个线程调用 SignalAndWait() 表示自己已抵达,并阻塞直到其余所有线程也调用该方法:
- 调用
SignalAndWait()是线程安全的,可被任意线程多次调用 - 首次所有线程都调用后,屏障“打开”,所有线程继续;同时屏障自动进入第二轮等待
- 若某线程提前退出(未调用
SignalAndWait()),其余线程将永久阻塞——这是最常见死锁原因 - 可传入一个
Action委托,在最后一人到达、所有人释放前执行一次(比如做本轮汇总)
示例:
var barrier = new Barrier(3, b => Console.WriteLine($"第 {b.CurrentPhaseNumber} 轮汇合完成"));
Task.Run(() => { Thread.Sleep(100); barrier.SignalAndWait(); });
Task.Run(() => { Thread.Sleep(200); barrier.SignalAndWait(); });
Task.Run(() => { Thread.Sleep(50); barrier.SignalAndWait(); });如何安全地提前退出或处理异常Barrier 本身不响应取消令牌,也不能被中断。若需支持取消或超时,必须自行包装:
- 不要依赖
Thread.Abort()(已废弃)或暴力中断线程
- 推荐用
CancellationToken 配合轮询 + WaitOne(timeout) 自建等待逻辑,或改用 Task.WhenAll() + Task.Delay() 组合模拟屏障行为
- 若某线程抛出异常,其他线程仍在
SignalAndWait() 中阻塞,异常不会自动传播——必须在外层 try/catch 分别捕获
-
barrier.Dispose() 后再调用 SignalAndWait() 会抛出 ObjectDisposedException
Barrier 与 CountdownEvent 的关键区别
两者都用于计数同步,但语义和生命周期完全不同:
-
CountdownEvent 是“一次性门闩”:初始化为 N,每次 Signal() 减 1,减到 0 后所有等待者释放,之后再 Wait() 会立即返回;无法重置(除非手动 Reset(N),但不推荐)
-
Barrier 是“循环路障”:每次 SignalAndWait() 都参与计数,全员到达即通关并自动开启下一轮;天生支持多阶段协作
- 性能上,
Barrier 内部使用无锁结构优化,高并发下比反复 Reset() 的 CountdownEvent 更稳定
Thread.Abort()(已废弃)或暴力中断线程CancellationToken 配合轮询 + WaitOne(timeout) 自建等待逻辑,或改用 Task.WhenAll() + Task.Delay() 组合模拟屏障行为SignalAndWait() 中阻塞,异常不会自动传播——必须在外层 try/catch 分别捕获barrier.Dispose() 后再调用 SignalAndWait() 会抛出 ObjectDisposedException
-
CountdownEvent是“一次性门闩”:初始化为 N,每次Signal()减 1,减到 0 后所有等待者释放,之后再Wait()会立即返回;无法重置(除非手动Reset(N),但不推荐) -
Barrier是“循环路障”:每次SignalAndWait()都参与计数,全员到达即通关并自动开启下一轮;天生支持多阶段协作 - 性能上,
Barrier内部使用无锁结构优化,高并发下比反复Reset()的CountdownEvent更稳定
真正容易被忽略的是:Barrier 的“阶段号”(CurrentPhaseNumber)从 0 开始,且每次全员通过后才加 1——这意味着你在回调里看到的 phase number,是本轮刚完成的序号,不是下一轮的。如果逻辑依赖阶段编号做状态切换,务必注意这个偏移。










