C# 异步方法性能陷阱 C# async void和async Task有什么区别

来源:这里教程网 时间:2026-02-21 17:40:23 作者:

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
,调用链上所有环节都得重新考虑并发、取消、超时和错误恢复。

相关推荐