c# 异步编程的优缺点

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

异步编程不会自动提升 CPU 密集型任务的性能

很多人误以为

async
/
await
能让计算变快,其实它只优化 I/O 等待时间。比如对一个大数组做排序、图像处理或加密解密,用
Task.Run(() => HeavyComputation())
包裹后,只是把工作扔到线程池里执行,并不减少总耗时,还增加了调度开销和上下文切换成本。

真正适合
async
的场景:HTTP 请求(
HttpClient.GetAsync
)、文件读写(
File.ReadAllTextAsync
)、数据库查询(
DbCommand.ExecuteReaderAsync
)——这些本质是等待操作系统完成 I/O,期间线程可被复用
若强行把纯计算逻辑标记为
async
且不配合
Task.Run
,编译器会警告“此 async 方法缺少 await”,运行时也仍是同步阻塞
高频调用小计算任务时,
Task.Run
反而比直接同步执行更慢,因为线程池排队 + 状态机分配有额外开销

ConfigureAwait(false) 不是可有可无的配置项

在类库或底层工具方法中漏掉

ConfigureAwait(false)
,可能引发死锁或 UI 响应卡顿。它的作用是告诉运行时:await 完成后**不要强制回调回原始同步上下文**(比如 WinForms 的 UI 线程、ASP.NET Classic 的
HttpContext
)。

ASP.NET Core 默认没有
SynchronizationContext
,所以多数情况下不加也不会出问题;但 ASP.NET Framework 或 WPF/WinForms 项目里,如果在 UI 线程调用
GetAwaiter().GetResult()
或错误地用了
.Result
,就极易死锁
类库作者必须默认加
ConfigureAwait(false)
,否则使用者在非 UI 环境引用该库时,可能因意外捕获上下文导致性能下降甚至异常
只有明确需要回到原上下文时才不加——比如更新 WPF 的
TextBox.Text
,必须在 UI 线程执行

异常堆栈容易丢失原始位置

异步方法抛出异常后,堆栈信息会包含状态机内部方法(如

MoveNext
),原始调用点可能被掩盖。尤其在多层
await
链路中,
InnerException
层级变深,调试时第一眼看不到出错的真实行号。

使用
try/catch
捕获异常时,别只看
e.ToString()
,要逐层检查
e.InnerException
.NET 5+ 支持
await using
和更清晰的异常传播,但旧项目若还在用 .NET Framework 4.7.2,建议在关键异步入口处手动包装异常:
try {
    await DoSomethingAsync();
}
catch (Exception ex) {
    throw new InvalidOperationException("调用 DoSomethingAsync 失败", ex);
}
单元测试中用
Assert.ThrowsAsync<t>()</t>
而不是
Assert.Throws<t>()</t>
,否则会误判为未抛异常

async void 是仅限事件处理程序的危险选择

async void
方法无法被
await
,异常会直接炸到
SynchronizationContext
或进程级,极难捕获。它唯一合理用途是 UI 事件处理器(如
Button_Click
)。

永远不要在业务逻辑、工具方法、API 接口里写
async void
;应该统一用
async Task
Web API 控制器中返回
Task<iactionresult></iactionresult>
是标准做法;写成
async void
会导致请求提前结束、日志缺失、监控失效
测试
async void
方法几乎不可能——没有返回值,没地方
await
,只能靠超时或副作用判断,可靠性极低
实际项目里最常被忽略的,是跨框架兼容性细节:比如
ValueTask
在 .NET Core 2.1+ 才稳定支持,老项目升级时若盲目替换
Task
,可能引入隐式装箱或生命周期错误;还有
async
方法里用
lock
会编译失败,必须改用
SemaphoreSlim.WaitAsync
。这些都不是理论问题,而是上线后才暴露的坑。

相关推荐

热文推荐