C# async函数调用:从入门到避坑指南

1次阅读
没有评论

共计 1779 个字符,预计需要花费 5 分钟才能阅读完成。

image.webp

为什么需要异步编程?

刚开始接触 C# 时,我总习惯用同步方式写代码——简单直接,一个方法调用完再执行下一个。直到有一天,我的 WPF 应用在加载数据时界面完全卡死,用户体验极差。这才意识到同步编程的核心问题:

C# async 函数调用:从入门到避坑指南

  • UI 线程阻塞:主线程被耗时操作(如网络请求)占用时,界面会冻结
  • 资源浪费:线程在等待 I / O 操作(如数据库查询)时完全闲置
  • 吞吐量低下:同步服务无法有效利用现代多核 CPU

异步编程核心机制

await 状态机工作原理

编译器会把 async 方法转换成状态机结构。举个例子:

public async Task<string> GetDataAsync()
{var data = await HttpClient.GetStringAsync("url");
    return data.ToUpper();}

实际会被编译为类似:

  1. 创建状态机对象
  2. 执行到 await 时:
  3. 如果操作未完成,返回未完成的 Task
  4. 注册后续代码为 continuation
  5. 操作完成后,通过 SynchronizationContext 回到原始上下文(如 UI 线程)
  6. 执行 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();
    }
}

五大避坑指南

  1. async void 陷阱
  2. async void 方法无法捕获异常
  3. 解决方案:始终使用 async Task 除非是事件处理器

  4. 死锁场景

  5. UI 线程调用.Result 导致死锁
  6. 解决方案:库代码使用 ConfigureAwait(false)

  7. 线程池饥饿

  8. 并行度过高耗尽线程池
  9. 解决方案:控制并发量,使用 SemaphoreSlim

  10. 任务类型混淆

  11. I/ O 密集型 vs CPU 密集型
  12. 解决方案:Task.Run 只用于 CPU 密集型

  13. 取消忽略

  14. 不处理 CancellationToken 导致资源浪费
  15. 解决方案:正确传递和检查 token

性能对比测试

使用 BenchmarkDotNet 测试同步 vs 异步的 HTTP 请求吞吐量:

方式 请求数 / 秒 内存分配
同步调用 1,200
异步调用 8,500

思考题

  1. 设计长时间任务时,应该如何暴露取消点?
  2. ASP.NET Core 的上下文模型与 WinForms 有何不同?
  3. 在什么场景下 ValueTask 比 Task 更合适?

总结建议

经过这段时间的异步编程实践,我有三个深刻体会:

  1. 异步思维转变 最难:需要从线性思维切换到任务流思维
  2. 错误处理更重要:异步链中的异常传播方式与同步代码不同
  3. 性能提升显著:合理使用异步后,我们的 Web API 吞吐量提升了 4 倍

建议刚开始接触异步时,先用简单示例理解 await 的工作流程,再逐步应用到实际项目中。遇到问题时,善用 VS 的调试工具观察任务状态和调用栈。

正文完
 0
评论(没有评论)