一、忘记处理异步异常

异步编程中最容易被忽略的就是异常处理。很多人以为用try-catch包裹await调用就万事大吉了,实际上异步异常会以更隐蔽的方式逃逸。

// 错误示例:异常被静默吞噬
async Task ProcessDataAsync()
{
    try
    {
        var data = await ReadFileAsync("data.json"); // 可能抛出IOException
        await SaveToDbAsync(data); // 可能抛出SqlException
    }
    catch (Exception ex)
    {
        Console.WriteLine("出错了"); // 仅打印日志,未向上传递
    }
}

// 正确做法:重新抛出或记录完整堆栈
async Task ProcessDataFixedAsync()
{
    try
    {
        var data = await ReadFileAsync("data.json");
        await SaveToDbAsync(data);
    }
    catch (IOException ioEx)
    {
        Log.Error(ioEx, "文件读取失败");
        throw; // 使用throw保留原始堆栈
    }
    catch (SqlException sqlEx)
    {
        await NotifyAdminAsync(sqlEx); // 异步记录
        throw new DataAccessException("数据库操作失败", sqlEx);
    }
}

关键点

  • 异步方法中的异常不会立即抛出,直到await才会触发
  • 使用throw;而非throw ex;保留原始调用堆栈
  • 考虑使用AggregateException处理并行任务异常

二、忽视ConfigureAwait(false)的使用

在非UI应用程序中,不加区分地使用await会导致不必要的上下文切换,甚至引发死锁。

// 典型死锁场景(WinForms/WPF)
private void Button_Click(object sender, EventArgs e)
{
    var result = GetDataAsync().Result; // 同步阻塞调用
    label.Text = result;
}

async Task<string> GetDataAsync()
{
    await Task.Delay(1000); // 默认捕获UI上下文
    return "data";
}

// 解决方案1:全链路异步
private async void Button_Click_Fixed(object sender, EventArgs e)
{
    label.Text = await GetDataFixedAsync();
}

async Task<string> GetDataFixedAsync()
{
    await Task.Delay(1000).ConfigureAwait(false); // 放弃上下文捕获
    return "data";
}

性能优化技巧

  • 类库代码始终使用ConfigureAwait(false)
  • UI层保留默认配置以更新控件
  • ASP.NET Core不需要配置,因为不存在同步上下文

三、异步流处理不当

处理I/O密集型操作时,错误的分块策略会导致内存暴涨或性能下降。

// 错误:一次性读取大文件
async Task<byte[]> ReadFullFileAsync(string path)
{
    using var fs = new FileStream(path, FileMode.Open);
    byte[] buffer = new byte[fs.Length];
    await fs.ReadAsync(buffer, 0, buffer.Length); // 可能OOM
    return buffer;
}

// 正确:分块流式处理
async Task ProcessLargeFileAsync(string path)
{
    const int bufferSize = 81920; // 80KB最佳实践值
    using var fs = new FileStream(path, FileMode.Open);
    byte[] buffer = new byte[bufferSize];
    
    int bytesRead;
    while ((bytesRead = await fs.ReadAsync(buffer, 0, bufferSize)) > 0)
    {
        await ProcessChunkAsync(buffer, bytesRead); // 处理数据块
    }
}

关联技术

  • 使用System.IO.Pipelines实现高性能流处理
  • IAsyncEnumerable<T>适合生成异步序列

四、错误的任务组合方式

同时执行多个任务时,不当的组合策略会导致意外行为。

// 危险做法:隐式并行
async Task SendNotificationsAsync(List<User> users)
{
    foreach (var user in users)
    {
        await SendEmailAsync(user); // 串行执行效率低下
    }
}

// 改进方案1:有限并发
async Task SendAllEmailsAsync(List<User> users)
{
    var tasks = users.Select(user => 
        SendEmailAsync(user).ContinueWith(t => 
        {
            if (t.IsFaulted) Log.Error(t.Exception);
        }));
    
    await Task.WhenAll(tasks); // 并行执行但无并发控制
}

// 最佳实践:使用SemaphoreSlim
private readonly SemaphoreSlim _semaphore = new(10);

async Task SendWithThrottlingAsync(List<User> users)
{
    var tasks = users.Select(async user =>
    {
        await _semaphore.WaitAsync();
        try
        {
            await SendEmailAsync(user);
        }
        finally
        {
            _semaphore.Release();
        }
    });
    
    await Task.WhenAll(tasks);
}

五、忽略取消令牌传播

未正确处理取消请求会导致资源泄漏和响应迟缓。

// 错误:忽略取消令牌
async Task LongRunningOperationAsync()
{
    await Task.Delay(Timeout.Infinite); // 无法取消
}

// 正确实现
async Task CancelableOperationAsync(CancellationToken ct)
{
    await Task.Delay(TimeSpan.FromMinutes(5), ct); // 支持取消
    ct.ThrowIfCancellationRequested(); // 显式检查
    
    using var cts = new CancellationTokenSource();
    var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(ct, cts.Token);
    
    await ProcessAsync(linkedToken.Token); // 组合令牌
}

应用场景

  • Web请求超时处理
  • 用户手动取消长时间操作
  • 后台任务优雅终止

六、状态共享引发的竞态条件

异步代码中共享可变状态是万恶之源。

// 典型竞态条件
class CounterService
{
    private int _count;
    
    public async Task IncrementAsync()
    {
        var current = _count;
        await Task.Delay(100); // 在此处可能被其他线程修改
        _count = current + 1;
    }
}

// 线程安全方案
class SafeCounter
{
    private int _count;
    private readonly object _lock = new();
    
    public async Task IncrementAsync()
    {
        lock (_lock)
        {
            var current = _count;
            // 临界区内不能有await!
        }
        
        await HeavyOperationAsync();
        
        lock (_lock)
        {
            _count++;
        }
    }
}

替代方案

  • 使用Immutable集合
  • 通过Channel实现生产者消费者模式
  • 采用Interlocked原子操作

总结

异步编程就像高空走钢丝,稍有不慎就会坠入深渊。记住这些黄金法则:

  1. 异常处理要像防毒面具一样严密
  2. ConfigureAwait是你的安全绳
  3. 流式处理记住"小口慢咽"
  4. 任务组合避免"一锅乱炖"
  5. 取消令牌是紧急制动闸
  6. 共享状态等于玩火自焚

掌握这些技巧后,你的异步代码将既健壮又高效,在复杂的业务场景中游刃有余。