TaskCompletionSource封装异步方法时Dispose导致await后代码不执行
问题原因
你遇到的挂起问题本质是TaskCompletionSource(以下简称TCS)关联的任务永远无法进入完成状态,结合你描述的两个复现条件,具体原因可以拆解为以下几点:
- 回调逻辑覆盖不全:你当前的回调仅处理了
ScanFail和ScanComplete两个通知码,其他场景(比如资源释放触发的中断通知、未知通知)到达时不会给TCS设置状态,导致任务永远处于 pending 状态,await会无限等待。 - 回调无异常捕获逻辑:如果回调触发时
_wlanInterface已经被释放,访问实例的操作会抛出异常,而事件回调中的异常通常会被第三方WiFi库吞掉,不会触发TCS的SetException,同样会导致任务永远无法完成。 - 资源释放时序冲突:你删除
Dispose后逻辑可正常运行,说明Dispose的执行实际早于扫描完成回调触发:多数WLAN封装库在Dispose时会主动注销所有事件、清空待派发的通知队列,导致你注册的扫描完成回调永远不会被执行,TCS无法完成。 - 同步上下文适配问题:原生WLAN API的通知依赖Windows消息队列派发,控制台程序默认没有消息循环。同步调用
GetResult()时,主线程处于阻塞状态,底层库会临时调度消息派发让回调正常触发;而await交出线程控制权后,没有消息循环处理通知,导致回调无法触发。
解决方案
1. 完善TCS的边界处理逻辑
修改ScanNetworksAsync方法,补全异常捕获、取消监听、释放监听逻辑,同时给TCS指定RunContinuationsAsynchronously避免回调阻塞通知线程:
public Task ScanNetworksAsync(CancellationToken cancellationToken = default) { // 配置TCS避免续期阻塞通知线程 var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); void Callback(Wlan.WlanNotificationData data) { try { switch (data.notificationCode) { case (int) Wlan.WlanNotificationCodeAcm.ScanFail: _wlanInterface!.WlanNotification -= Callback; tcs.TrySetResult(false); break; case (int) Wlan.WlanNotificationCodeAcm.ScanComplete: _wlanInterface!.WlanNotification -= Callback; tcs.TrySetResult(true); break; } } catch (Exception ex) { _wlanInterface!.WlanNotification -= Callback; tcs.TrySetException(ex); } } // 监听外部取消请求 var cancellationReg = cancellationToken.Register(() => { _wlanInterface!.WlanNotification -= Callback; tcs.TrySetCanceled(cancellationToken); }); // 监听对象释放事件(需要你在封装类中添加Disposed事件,Dispose时触发) Disposed += (_, _) => { _wlanInterface!.WlanNotification -= Callback; tcs.TrySetException(new ObjectDisposedException(nameof(WiFiClient))); }; _wlanInterface!.WlanNotification += Callback; try { _wlanInterface.Scan(); // 任务完成后释放取消注册资源 tcs.Task.ContinueWith(_ => cancellationReg.Dispose(), cancellationToken); } catch (Exception e) { _wlanInterface.WlanNotification -= Callback; cancellationReg.Dispose(); tcs.TrySetException(e); } return tcs.Task; }
2. 调整Dispose逻辑避免提前销毁资源
如果扫描完成前不需要释放资源,可以在Dispose时等待扫描任务完成,避免通知被清空:
private Task? _ongoingScanTask; // 扫描方法中添加正在运行的任务标记 public Task ScanNetworksAsync(CancellationToken cancellationToken = default) { // ... 其他逻辑不变 _ongoingScanTask = tcs.Task; _ongoingScanTask.ContinueWith(_ => _ongoingScanTask = null, cancellationToken); return _ongoingScanTask; } public void Dispose() { // 最多等待1秒扫描结束,避免无限阻塞 _ongoingScanTask?.Wait(1000); ((IDisposable) _internalClient).Dispose(); }
3. 适配控制台程序的消息循环
可以在Main方法入口强制指定线程池同步上下文,让通知可以在线程池派发:
private static async Task Main(string[] args) { // 入口第一行添加 SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); _container = new Startup().BuildContainer(); // ... 其他逻辑不变 }
4. 调整依赖注入Scope的释放时机
如果后续SubscribeEvents方法也需要用到WiFi客户端,可以把客户端注册为全局单例,或者把后续逻辑移到using块内部,避免Scope释放导致资源被提前销毁。
内容的提问来源于stack exchange,提问作者ikcehcus
相关产品推荐
相关产品推荐

