一、忘记处理异步异常
异步编程中最容易被忽略的就是异常处理。很多人以为用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原子操作
总结
异步编程就像高空走钢丝,稍有不慎就会坠入深渊。记住这些黄金法则:
- 异常处理要像防毒面具一样严密
ConfigureAwait是你的安全绳- 流式处理记住"小口慢咽"
- 任务组合避免"一锅乱炖"
- 取消令牌是紧急制动闸
- 共享状态等于玩火自焚
掌握这些技巧后,你的异步代码将既健壮又高效,在复杂的业务场景中游刃有余。
评论