C#异步编程五大陷阱:躲开这些坑,别让async/await拖垮你的应用

作者:微信公众号:【架构师老卢】
8-2 8:21
17

大多数C#开发者使用async/await是因为它简单易用。它看起来像同步代码,感觉安全,而且效果很好——直到你的应用上线生产环境,性能开始显著下降。

关键在于:异步(Async)并非总是零成本的,滥用它可能导致线程池饥饿(thread pool starvation)内存压力(memory pressure) 以及可伸缩性问题(scalability issues)

1. 避免使用 async void —— 事件处理器除外 这是一个陷阱。

public async void DoSomethingAsync()
{
    await Task.Delay(1000);
}

问题在哪?你无法等待(await)它,无法清晰地处理异常,而且它的行为就像一个即发即弃(fire-and-forget) 的线程。请始终使用:

public async Task DoSomethingAsync()

除了在事件处理器(event handlers) 中,其他情况应 100% 避免使用 async void

2. 不要在异步代码上阻塞(Block) 永远不要这样做:

var result = GetDataAsync().Result;

或者这样:

var result = GetDataAsync().GetAwaiter().GetResult();

这可能导致你的应用死锁(deadlock),尤其是在 ASP.NET 或 UI 上下文中。它会占用线程,严重损害可伸缩性。

使用以下方式修复:

var result = await GetDataAsync();

如果你绝对必须(absolutely must) 在同步方法中调用异步方法,在异步方法中使用 .ConfigureAwait(false) 来避免捕获上下文(context capture)。

3. 在类库中使用 ConfigureAwait(false) 每次你使用 await 时,默认行为(default behavior) 是在原始的同步上下文(synchronization context) 上恢复执行——这对于 UI 应用很棒,但在后台类库或服务中通常不需要。

await SomeIoBoundCall().ConfigureAwait(false);

这可以减少上下文切换(context switching),提高服务器端代码的吞吐量(throughput)

4. 在紧密循环中最小化异步开销 不要盲目地将每个方法都设为 async

public async Task<int> GetNumberAsync()
{
    return 42;
}

这会不必要地分配一个状态机(state machine)。相反,直接返回 Task.FromResult(42)

public Task<int> GetNumberAsync() => Task.FromResult(42);

仅在需要时才使用 async。如果方法内部没有实际的 await 操作,使用 async 就纯粹是开销(overhead)

5. 使用 BenchmarkDotNet 或 PerfView 进行性能度量 不要假设。使用 BenchmarkDotNetPerfView 在实际场景中验证你的假设。异步并不总是意味着更快——尤其是对于 CPU密集型(CPU-bound) 任务。

总结

  • 使用 async Task,而非 async void
  • 永远不要在异步代码上使用 .Result.Wait()
  • 类库(libraries) 中使用 .ConfigureAwait(false)
  • 如果方法内部没有实际的 await,就避免使用 async
  • 真实负载条件(real load conditions) 下对一切进行基准测试(Benchmark)

Async/await 是一项超能力(superpower) —— 但请明智地使用它。

相关留言评论
昵称:
邮箱:
阅读排行