共计 1779 个字符,预计需要花费 5 分钟才能阅读完成。
为什么需要异步编程?
刚开始接触 C# 时,我总习惯用同步方式写代码——简单直接,一个方法调用完再执行下一个。直到有一天,我的 WPF 应用在加载数据时界面完全卡死,用户体验极差。这才意识到同步编程的核心问题:

- UI 线程阻塞:主线程被耗时操作(如网络请求)占用时,界面会冻结
- 资源浪费:线程在等待 I / O 操作(如数据库查询)时完全闲置
- 吞吐量低下:同步服务无法有效利用现代多核 CPU
异步编程核心机制
await 状态机工作原理
编译器会把 async 方法转换成状态机结构。举个例子:
public async Task<string> GetDataAsync()
{var data = await HttpClient.GetStringAsync("url");
return data.ToUpper();}
实际会被编译为类似:
- 创建状态机对象
- 执行到 await 时:
- 如果操作未完成,返回未完成的 Task
- 注册后续代码为 continuation
- 操作完成后,通过 SynchronizationContext 回到原始上下文(如 UI 线程)
- 执行 continuation 代码
SynchronizationContext 的关键作用
这个上下文调度器决定了 await 之后代码在哪里执行:
- UI 程序(WPF/WinForms):自动回到 UI 线程
- ASP.NET Core:没有特定上下文,默认在线程池执行
- 控制台程序:默认继续在线程池线程执行
实战代码示例
基础 HTTP 请求
public async Task<string> GetWebsiteContentAsync(CancellationToken ct)
{
try
{using var client = new HttpClient();
return await client.GetStringAsync("https://example.com", ct);
}
catch (TaskCanceledException)
{Console.WriteLine("请求被取消");
return string.Empty;
}
}
并发任务处理
public async Task<string[]> DownloadMultipleAsync(IEnumerable<string> urls)
{
var downloadTasks = urls.Select(url =>
HttpClient.GetStringAsync(url)).ToList();
return await Task.WhenAll(downloadTasks);
}
异步流处理
public async IAsyncEnumerable<string> ReadLinesAsync(string filePath)
{using var reader = new StreamReader(filePath);
while (!reader.EndOfStream)
{yield return await reader.ReadLineAsync();
}
}
五大避坑指南
- async void 陷阱
- async void 方法无法捕获异常
-
解决方案:始终使用 async Task 除非是事件处理器
-
死锁场景
- UI 线程调用.Result 导致死锁
-
解决方案:库代码使用 ConfigureAwait(false)
-
线程池饥饿
- 并行度过高耗尽线程池
-
解决方案:控制并发量,使用 SemaphoreSlim
-
任务类型混淆
- I/ O 密集型 vs CPU 密集型
-
解决方案:Task.Run 只用于 CPU 密集型
-
取消忽略
- 不处理 CancellationToken 导致资源浪费
- 解决方案:正确传递和检查 token
性能对比测试
使用 BenchmarkDotNet 测试同步 vs 异步的 HTTP 请求吞吐量:
| 方式 | 请求数 / 秒 | 内存分配 |
|---|---|---|
| 同步调用 | 1,200 | 高 |
| 异步调用 | 8,500 | 低 |
思考题
- 设计长时间任务时,应该如何暴露取消点?
- ASP.NET Core 的上下文模型与 WinForms 有何不同?
- 在什么场景下 ValueTask 比 Task 更合适?
总结建议
经过这段时间的异步编程实践,我有三个深刻体会:
- 异步思维转变 最难:需要从线性思维切换到任务流思维
- 错误处理更重要:异步链中的异常传播方式与同步代码不同
- 性能提升显著:合理使用异步后,我们的 Web API 吞吐量提升了 4 倍
建议刚开始接触异步时,先用简单示例理解 await 的工作流程,再逐步应用到实际项目中。遇到问题时,善用 VS 的调试工具观察任务状态和调用栈。
正文完
