C# 异步编程:从入门到精通的最佳实践

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 平台的并发处理能力。