C#未按序触发的配对异步方法,如何实现A_i→对应B_i有序执行
更简洁的实现思路
你现在的实现本质上是靠两个互斥量强行给A、B的调用分别排队,再用TCS做跨类型的唤醒,其实完全可以跳出“分别排队、互相唤醒”的思路,换成基于状态的流水线调度——毕竟你的场景有两个非常明确的前置条件:
- A、B本身的触发就是严格按序号顺序来的,不存在插队、乱序触发的情况
- 最终要的执行序列是固定的
A₁→B₁→A₂→B₂→A₃→B₃...循环
这种场景根本不需要复杂的FIFO信号量,只要维护一个简单的状态机,明确“当前步骤该执行A还是B”,所有进来的调用只要匹配当前步骤就直接执行,不匹配就挂起等唤醒就行,整个代码量能减少一半,还能避开原来实现里的几个常见坑。
具体实现
你可以把这套调度逻辑封装成一个独立的协调器类,核心只需要一个锁、一个步骤计数、一个等待TCS,完全不需要额外的信号量:
public class SequentialABRunner { // 记录下一个要执行的步骤:偶数步对应A,奇数步对应B,从0开始(第一步是A1) private int _nextStep = 0; // 挂起等待的任务入口,因为触发本身是严格有序的,只需要存一个就行 private TaskCompletionSource _waitHandle; // 保护共享状态的轻量锁 private readonly object _stateLock = new(); // A方法的统一入口 public async Task RunAAsync(Func<Task> aWork) { Task runTask; lock (_stateLock) { // 当前该执行偶数步的A,直接执行 if (_nextStep % 2 == 0) { _nextStep++; runTask = ExecuteWorkWithStateUpdate(aWork); } // 还没轮到A,挂起等唤醒 else { _waitHandle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); runTask = _waitHandle.Task.ContinueWith(async _ => await ExecuteWorkWithStateUpdate(aWork), TaskScheduler.Default); } } await runTask; } // B方法的统一入口 public async Task RunBAsync(Func<Task> bWork) { Task runTask; lock (_stateLock) { // 当前该执行奇数步的B,直接执行 if (_nextStep % 2 == 1) { _nextStep++; runTask = ExecuteWorkWithStateUpdate(bWork); } // 还没轮到B,挂起等唤醒 else { _waitHandle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); runTask = _waitHandle.Task.ContinueWith(async _ => await ExecuteWorkWithStateUpdate(bWork), TaskScheduler.Default); } } await runTask; } // 执行完工作后更新状态,唤醒下一个等待的任务 private async Task ExecuteWorkWithStateUpdate(Func<Task> work) { await work(); lock (_stateLock) { _waitHandle?.TrySetResult(); _waitHandle = null; } } }
方案优势
- 没有冗余逻辑:不需要分别为A、B建两个FIFO信号量,因为你本来就保证了触发顺序是严格有序的,根本不需要额外的排队机制。整个调度逻辑只靠状态计数推进,就算原来的FIFO信号量实现有唤醒顺序的bug,这套逻辑也不会出错。
- 状态更可控:所有共享状态都在锁的保护下更新,不存在原来实现里TCS为null或者错配的问题。你甚至可以很轻松地在状态计数里加上序号校验,就算出现重复调用、漏调用的异常场景,也能快速定位问题。
- 避坑更彻底:原实现里用
async void定义异步方法是非常危险的——只要方法里抛出未捕获异常,会直接导致进程崩溃。这套实现的入口都是async Task,可以正常向上传递异常、支持外部await,不会出现方法执行完就“消失”的问题。
适配原有代码
原来的两个方法只需要把核心逻辑抽到Work里,用协调器包一层就行:
var runner = new SequentialABRunner(); // 替换原来的AAsync async Task AAsync() { await runner.RunAAsync(async () => { // 原来AAsync里的核心逻辑 await Task.Delay(100); // 模拟异步操作 }); } // 替换原来的BAsync async Task BAsync() { await runner.RunBAsync(async () => { // 原来BAsync里的核心逻辑 await Task.Delay(100); // 模拟异步操作 }); }
用你举的乱序触发序列 A₁A₂B₁B₂A₃B₃B₄A₄ 测试,最终实际的执行顺序会严格按照要求输出 A₁→B₁→A₂→B₂→A₃→B₃→A₄→B₄,完全匹配需求。
内容的提问来源于stack exchange,提问作者Miguel
相关产品推荐
相关产品推荐

