C# 的 async/await 关键字让异步编程变得前所未有的简单,但这种简洁性有时会隐藏底层的复杂性。本文将深入探讨异步编程的最佳实践,帮助你写出更高效、更可靠的异步代码。
async/await 基础回顾
async 和 await 是语法糖,编译器会将它们转换为状态机。关键点在于:
- async 方法:返回 Task 或 Task<T>,调用时不会阻塞线程
- await 表达式:等待任务完成,期间释放线程
- ConfigureAwait(false):跨线程上下文优化性能
// 基础异步方法
public async Task<User> GetUserAsync(int id)
{
var user = await _userRepository.FindByIdAsync(id);
return user;
}
// 库代码中的最佳实践
public async Task<IEnumerable<User>> GetAllUsersAsync()
{
var users = await _userRepository.GetAllAsync()
.ConfigureAwait(false); // 不需要同步上下文
return users;
}
常见陷阱与解决方案
1. 死锁问题
// ❌ 危险:在 UI 线程或 ASP.NET 同步上下文中调用
var user = GetUserAsync(id).Result; // 可能死锁!
// ✅ 安全:始终使用 await
var user = await GetUserAsync(id);
2. 异常处理
// ❌ 错误:异常被吞掉
try
{
await Task.Run(() => { throw new Exception("Oops"); });
}
finally
{
// 这里捕获不到异常
}
// ✅ 正确:多个任务并行,统一处理异常
var tasks = new List<Task>
{
Task1Async(),
Task2Async(),
Task3Async()
};
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// 只能看到第一个异常
Console.WriteLine($"Exception: {ex.Message}");
}
3. void 返回类型
// ❌ 避免:async void 方法
public async void BadMethod()
{
await Task.Delay(1000);
// 异常无法被外部捕获!
}
// ✅ 推荐:使用 Task 返回类型
public async Task GoodMethod()
{
await Task.Delay(1000);
// 异常可以被 await 或 .Result 捕获
}
性能优化技巧
1. 避免不必要的 async
// ❌ 浪费:不需要 async
public async Task<int> GetValueAsync()
{
return await ComputeValueAsync(); // 不必要的 await
}
// ✅ 高效:直接返回 Task
public Task<int> GetValueAsync()
{
return ComputeValueAsync();
}
2. ValueTask 优化
// 高频缓存场景:使用 ValueTask 减少内存分配
public async ValueTask<User> GetUserAsync(int id)
{
// 快速路径:缓存命中,无需分配 Task 对象
if (_cache.TryGetValue(id, out var user))
return user;
// 慢速路径:需要异步查询
user = await _repository.FindByIdAsync(id);
_cache[id] = user;
return user;
}
3. 并行处理
// ❌ 串行:慢
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id);
var payments = await GetPaymentsAsync(id);
// ✅ 并行:快 3 倍
var userTask = GetUserAsync(id);
var ordersTask = GetOrdersAsync(id);
var paymentsTask = GetPaymentsAsync(id);
await Task.WhenAll(userTask, ordersTask, paymentsTask);
var user = await userTask;
var orders = await ordersTask;
var payments = await paymentsTask;
取消支持
长时间运行的操作应该支持取消:
public async Task<IEnumerable<User>> GetUsersAsync(
CancellationToken cancellationToken = default)
{
var users = new List<User>();
await foreach (var user in _repository.StreamUsersAsync(cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
users.Add(user);
}
return users;
}
// 使用示例
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var users = await GetUsersAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("操作被取消");
}
实战案例:异步 HTTP 请求
public class HttpClientWrapper
{
private readonly HttpClient _httpClient;
private readonly ILogger<HttpClientWrapper> _logger;
public HttpClientWrapper(
HttpClient httpClient,
ILogger<HttpClientWrapper> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<T> GetAsync<T>(
string url,
CancellationToken cancellationToken = default)
{
try
{
var response = await _httpClient
.GetAsync(url, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var content = await response
.Content
.ReadAsStringAsync(cancellationToken)
.ConfigureAwait(false);
return JsonSerializer.Deserialize<T>(content);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP 请求失败: {Url}", url);
throw;
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON 反序列化失败: {Url}", url);
throw;
}
}
}
总结
C# 异步编程的核心原则:
- 始终使用 await:避免 .Result 或 .Wait() 导致的死锁
- 避免 async void:使用 Task 返回类型以便异常处理
- 优化性能:使用 ConfigureAwait(false)、ValueTask、并行处理
- 支持取消:长时间运行的操作应该接受 CancellationToken
- 正确处理异常:使用 try/catch 捕获异步异常
掌握这些技巧,你就能写出高效、可靠的异步代码,充分发挥 .NET 平台的并发处理能力。