如何在C#中实现WaitUntilEvent?串口通信场景下异步等待事件触发或超时的实现方案
解决方案:用
TaskCompletionSource实现异步等待事件或超时 这个场景用TaskCompletionSource来解决最适合不过了——它能帮你把事件触发转换成可等待的Task,再结合Task.WhenAny就能轻松实现「等待事件触发或超时」的异步逻辑,完全不会阻塞UI线程。下面是针对你业务场景的具体实现:
核心思路
- 创建
TaskCompletionSource对象,它相当于一个"异步信号器",事件触发时我们用它来标记任务完成。 - 临时订阅目标事件,事件触发时调用
TrySetResult完成TaskCompletionSource的任务。 - 用
Task.WhenAny同时等待事件触发任务和超时任务(由Task.Delay创建),哪个先完成就执行对应逻辑。 - 最后一定要取消事件订阅,避免内存泄漏。
具体代码实现
1. 实现AskHardwareIfReadyAndWaitForAnswer方法
private async Task AskHardwareIfReadyAndWaitForAnswer() { // 创建TaskCompletionSource,用bool标记是否成功收到响应 using var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); // 定义事件处理逻辑:硬件就绪时完成任务 void HardwareReadyHandler(object? sender, EventArgs e) { tcs.TrySetResult(true); } try { // 订阅硬件就绪事件 HardwareSaysItIsReadyEvent += HardwareReadyHandler; // 发送询问消息 serialPort.WriteLine("Are you ready?"); // 等待事件触发 或 1秒超时 var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000)); if (completedTask == tcs.Task) { // 收到硬件响应,继续后续流程 Console.WriteLine("硬件已就绪,准备发送问题"); } else { // 超时处理:抛出异常或执行自定义逻辑 throw new TimeoutException("等待硬件就绪超时(1秒)"); } } finally { // 必须取消订阅,避免内存泄漏和重复触发 HardwareSaysItIsReadyEvent -= HardwareReadyHandler; } }
2. 实现AskHardwareIfQuestionWasReceivedAndWaitForAnswer方法
这个方法需要先发送确认消息再完成等待,逻辑稍有不同:
private async Task AskHardwareIfQuestionWasReceivedAndWaitForAnswer() { using var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); void PcReadyHandler(object? sender, EventArgs e) { // 先发送PC就绪的确认消息 serialPort.WriteLine("yes, go ahead"); // 完成任务,继续接收答案的逻辑 tcs.TrySetResult(true); } try { HardwareAsksIfPcIsReadyEvent += PcReadyHandler; serialPort.WriteLine("Did you get it?"); var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000)); if (completedTask != tcs.Task) { throw new TimeoutException("等待硬件确认收到问题超时(1秒)"); } } finally { HardwareAsksIfPcIsReadyEvent -= PcReadyHandler; } }
优化:封装通用等待方法
为了避免重复代码,可以把等待逻辑封装成通用方法:
private async Task WaitForEventOrTimeout(EventHandler eventToWaitFor, Action sendMessage, int timeoutMs = 1000) { using var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); void EventHandlerWrapper(object? sender, EventArgs e) => tcs.TrySetResult(true); try { eventToWaitFor += EventHandlerWrapper; sendMessage(); var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs)); if (completedTask != tcs.Task) { throw new TimeoutException($"等待事件触发超时({timeoutMs}ms)"); } } finally { eventToWaitFor -= EventHandlerWrapper; } }
调用时就会非常简洁:
// 等待硬件就绪 await WaitForEventOrTimeout(HardwareSaysItIsReadyEvent, () => serialPort.WriteLine("Are you ready?")); // 等待硬件确认收到问题 await WaitForEventOrTimeout(HardwareAsksIfPcIsReadyEvent, () => serialPort.WriteLine("Did you get it?"));
关键注意事项
- 必须取消事件订阅:如果不取消,事件处理器会一直绑定在事件上,导致内存泄漏,甚至后续触发多次回调。
- 使用
RunContinuationsAsynchronously:确保任务完成时的回调不会阻塞SerialPort的DataReceived线程(该线程是后台线程,阻塞可能导致通信异常)。 - 异常处理:可以根据业务需求调整超时逻辑,比如不抛出异常而是返回bool值标记是否超时。
内容的提问来源于stack exchange,提问作者Ahmad
相关产品推荐
相关产品推荐

