async void 是无法被等待的“火药桶”
它不返回
Task,调用方完全无法感知其生命周期——既不能
await,也不能通过
.Wait()或
.Result同步等待,更无法捕获异常(未处理的异常会直接炸到线程池,导致进程崩溃)。只应出现在事件处理器中(如
Button_Click),且必须确保内部所有异步操作都显式错误处理。
常见错误现象:
- UI 线程上
async void抛出异常 → 应用直接退出,无堆栈可查
- 单元测试中调用
async void方法 → 测试立即结束,异步逻辑根本没执行完就断言失败
实操建议:
- 一律避免在业务逻辑、工具方法、服务层使用
async void
- 若必须用于事件,用
try/catch包裹全部 await 表达式,或委托给
async Task方法并用
FireAndForget模式(需自行记录异常)
- 检查现有代码:搜索
async void+
await组合,99% 都该重构
async Task 是可控、可组合、可监控的标准单元
async Task返回一个可等待的
Task对象,调用方可选择
await(推荐)、
.Wait()(阻塞,慎用)、或参与
Task.WhenAll等组合。异常会被封装进
Task,只有在
await或
.Wait()时才抛出,便于集中处理。
性能与兼容性影响:
-
async Task有极小的堆分配开销(
Task对象),但现代 .NET(6+)对空
Task和短生命周期
Task做了大量优化
- 返回
Task的方法可被
ConfigureAwait(false)控制同步上下文,避免 UI 线程争抢;
async void完全不支持此配置
- 所有诊断工具(如 dotTrace、Application Insights)都能正确追踪
Task生命周期;
async void在调用栈里直接“消失”
实操建议:
- 业务方法、服务接口、工具函数,一律返回
Task或
Task<t></t>
- 不要为“这个方法其实不 await 任何东西”而退化成
async void或同步实现——哪怕只是
return Task.CompletedTask;
- 避免无意义的
async/await套壳(如
async Task Foo() => await Bar();),直接返回
Bar()更高效
async Task vs async void 在异常传播上的本质差异
关键区别不在语法,而在异常是否被“捕获并挂起”。
async void中的异常会立即作为未观察异常(
UnobservedTaskException)触发,.NET 5+ 默认终止进程;而
async Task中的异常被压入
Task.Exception,直到有人消费这个
Task。
示例对比:
async void Bad() => await Task.Delay(100).ContinueWith(_ => throw new InvalidOperationException());
→ 进程大概率崩溃
async Task Good() => await Task.Delay(100).ContinueWith(_ => throw new InvalidOperationException());
→ 异常静默存在,
await Good()时才抛出,可被
try/catch捕获
容易踩的坑:
- 在
async void中调用
Task.Run(() => { throw ... }) → 异常永不被捕获- 认为 “我加了 try/catch 就安全了”,却忽略了
async void中
catch只能捕获同步部分,await 后的异常仍会逃逸
- 日志框架(如 Serilog)的异步写入若放在
async void里,可能日志根本没刷出就进程退出
如何快速识别和修复现有 async void 误用
最危险的是把
async void当作“后台任务启动器”用,比如:
async void StartBackgroundWork() => await LongRunningJob();—— 这等于放任一个无人看管的异步操作在后台自生自灭。
实操步骤:
- 用 Visual Studio “查找全部引用”或正则
async\s+void\s+\w+\s*\([^)]*\)扫描项目
- 对每个命中项,确认是否属于 UI 事件处理器(如命名含
Click、
Loaded、
Changed)
- 非事件处理器:改为
async Task,调用处补
await;若调用方是同步上下文(如旧版 ASP.NET),改用
GetAwaiter().GetResult()(仅限不得已)
- 事件处理器中:提取核心逻辑到
async Task方法,原
async void中仅做
TryCatchAwait包装(示例:
try { await DoWorkAsync(); } catch (Exception ex) { Log.Error(ex); })
真正难处理的不是语法转换,而是那些隐式依赖“方法执行完就结束”的同步假设——一旦改成可等待的
Task,调用链上所有环节都得重新考虑并发、取消、超时和错误恢复。
