c# 异步编程的最佳实践 c# async await 的坑

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

别在
async void
里写业务逻辑

这是最常踩的坑:只要不是事件处理函数(比如按钮点击),就绝不要用

async void
。它无法被
await
,异常会直接炸到同步上下文,导致应用崩溃且难以捕获。

async void
方法一旦抛出未处理异常,会触发
AppDomain.UnhandledException
TaskScheduler.UnobservedTaskException
,调试时根本看不到堆栈归属
正确做法是统一返回
Task
Task<t></t>
,哪怕不 await 也得用
async Task
唯一合理使用
async void
的场景:WPF/WinForms/UWP 的事件处理器,例如
private async void Button_Click(...)

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

在类库、底层工具方法、ASP.NET Core 中间件等**不依赖同步上下文**的场景下,漏掉

ConfigureAwait(false)
可能引发死锁或性能下降——尤其在 UI 线程或旧版 ASP.NET(非 Core)中。

默认
await
会尝试捕获当前
SynchronizationContext
并回调回去;服务端代码不需要这个行为,反而造成线程争抢
ASP.NET Core 已移除默认
SynchronizationContext
,但为兼容性与明确意图,仍建议显式写
.ConfigureAwait(false)
例外:WPF/WinForms 中更新 UI 控件必须回到主线程,此时不能加
ConfigureAwait(false)
,否则
Dispatcher.Invoke
类操作会失败

别用
Task.Run
包裹已异步的 I/O 操作

HttpClient.GetStringAsync()
FileStream.ReadAsync()
再套一层
Task.Run(() => ...)
,等于用线程池线程去等 I/O 完成,纯属浪费资源。

I/O 异步本质是基于完成端口(IOCP)或 epoll/kqueue,不占线程;
Task.Run
却强行调度到线程池,增加调度开销和上下文切换
只有 CPU 密集型工作(如 JSON 解析、图像处理)才适合用
Task.Run
脱离当前上下文
错误示例:
await Task.Run(() => httpClient.GetStringAsync(url)); // ❌
正确写法:
await httpClient.GetStringAsync(url); // ✅

警惕
async
方法里的同步阻塞调用

async
方法中调用
.Result
.Wait()
GetAwaiter().GetResult()
,极易引发死锁,尤其在有同步上下文的环境(如 WinForms 主线程、旧版 ASP.NET)。

这些调用会阻塞当前线程,而 await 后续回调又需要该线程空闲来执行,形成循环等待 即使没死锁,也会降低吞吐量——线程被卡住就不能干别的事 如果必须同步获取结果(极少见),优先考虑重构为真正异步链路;实在不行,用
await task.ConfigureAwait(false)
+
GetAwaiter().GetResult()
(仅限无上下文环境)
实际项目中最难排查的,往往是跨层调用时某处悄悄用了
.Result
,或者类库作者忘了加
ConfigureAwait(false)
。异步不是加几个关键字就完事,关键在整条调用链是否真正“放手”。

相关推荐