Skip to main content

异步编程 (Async/Await)

什么是异步编程?

核心概念

异步编程是一种编程范式,它允许程序在等待长时间运行的操作(如I/O操作、网络请求等)完成时,不阻塞当前线程,从而提高程序的响应性和吞吐量。

为什么需要异步编程?

🚀 提高响应性 - UI应用不会因为耗时操作而卡顿

⚡ 提升性能 - 服务器应用可以处理更多并发请求

💰 节省资源 - 减少线程数量,降低内存消耗

同步 vs 异步

public string GetDataSync()
{
// 阻塞当前线程,直到操作完成
var data = DownloadData(); // 假设需要5秒
ProcessData(data);
return "完成";
}

// 调用时会阻塞
var result = GetDataSync(); // 等待5秒
Console.WriteLine(result);
问题
  • 线程被阻塞,无法处理其他任务
  • UI应用会出现"无响应"
  • Web应用会占用线程池资源

Async/Await 基础用法

1. 基本语法

// async 关键字标记方法为异步方法
public async Task<int> CalculateAsync()
{
// await 关键字等待异步操作完成
await Task.Delay(1000); // 模拟耗时操作
return 42;
}

// 无返回值的异步方法
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("Work completed");
}

2. 返回类型规则

返回类型使用场景示例
Task无返回值的异步方法async Task DoWorkAsync()
Task<T>有返回值的异步方法async Task<int> GetNumberAsync()
ValueTask<T>性能敏感,可能同步完成async ValueTask<int> GetCachedAsync()
void⚠️ 仅用于事件处理器async void Button_Click(...)
避免使用 async void

除了事件处理器外,应避免使用 async void

  • 无法 await
  • 异常无法被捕获
  • 难以测试

实战案例

案例1:并行执行多个异步任务

public async Task<string> GetAllDataSequentialAsync()
{
// 总耗时:3 + 2 + 4 = 9秒
var user = await GetUserAsync(); // 3秒
var orders = await GetOrdersAsync(); // 2秒
var products = await GetProductsAsync(); // 4秒

return $"{user}, {orders}, {products}";
}

案例2:超时控制

public async Task<string> GetDataWithTimeoutAsync()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

try
{
var data = await GetDataAsync(cts.Token);
return data;
}
catch (OperationCanceledException)
{
return "操作超时";
}
}

案例3:异步流 (IAsyncEnumerable)

.NET Core 3.0+
// 生成异步流
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100); // 模拟异步操作
yield return i;
}
}

// 消费异步流
public async Task ConsumeNumbersAsync()
{
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number);
}
}

常见问题与最佳实践

❌ 错误做法

1. 阻塞异步代码(死锁风险)
// ❌ 不要这样做!可能导致死锁
public void BadMethod()
{
var result = GetDataAsync().Result; // 阻塞等待
var result2 = GetDataAsync().GetAwaiter().GetResult(); // 同样不好
}
死锁原因

在UI或ASP.NET同步上下文中,.Result.Wait() 会阻塞当前线程,而异步方法完成后需要回到原线程,导致死锁。

2. async void 方法
// ❌ 不要这样做!
public async void ProcessDataAsync()
{
await Task.Delay(1000);
throw new Exception("无法捕获!");
}

// ✅ 应该这样做
public async Task ProcessDataAsync()
{
await Task.Delay(1000);
// 异常可以被调用方捕获
}
3. 不必要的 async/await
// ❌ 不必要的 async/await
public async Task<string> GetDataAsync()
{
return await _httpClient.GetStringAsync(url);
}

// ✅ 直接返回 Task
public Task<string> GetDataAsync()
{
return _httpClient.GetStringAsync(url);
}

✅ 最佳实践

异步编程指南
  1. 一路异步到底 - 不要混用同步和异步代码
  2. 使用 ConfigureAwait(false) - 在类库代码中避免捕获上下文
  3. 取消令牌 - 长时间运行的操作应支持取消
  4. 避免 async void - 除了事件处理器
  5. 并行执行 - 使用 Task.WhenAll 并行执行独立任务
  6. 异常处理 - 使用 try-catch 包裹 await 语句

ConfigureAwait 详解

// 在类库代码中
public async Task<string> GetDataAsync()
{
// ConfigureAwait(false) 避免捕获同步上下文,提高性能
var data = await _httpClient.GetStringAsync(url).ConfigureAwait(false);
return ProcessData(data);
}

// 在UI代码中
private async void Button_Click(object sender, EventArgs e)
{
// 不使用 ConfigureAwait,需要回到UI线程更新界面
var data = await GetDataAsync();
textBox.Text = data; // 必须在UI线程执行
}
何时使用 ConfigureAwait(false)?
  • ✅ 类库代码
  • ✅ 不需要特定同步上下文的代码
  • ❌ 需要更新UI的代码
  • ❌ 需要访问 HttpContext 的ASP.NET代码

Web API 中的异步

ASP.NET Core Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;

public ProductsController(IProductService productService)
{
_productService = productService;
}

[HttpGet]
public async Task<ActionResult<List<Product>>> GetProductsAsync()
{
// 异步查询数据库
var products = await _productService.GetAllAsync();
return Ok(products);
}

[HttpPost]
public async Task<ActionResult<Product>> CreateProductAsync(Product product)
{
// 异步保存数据
var created = await _productService.CreateAsync(product);
return CreatedAtAction(nameof(GetProductsAsync), new { id = created.Id }, created);
}
}

性能对比

基准测试示例
public class AsyncBenchmark
{
[Benchmark]
public void SyncMethod()
{
// 同步处理100个请求
for (int i = 0; i < 100; i++)
{
Thread.Sleep(10); // 模拟IO操作
}
}

[Benchmark]
public async Task AsyncMethod()
{
// 异步处理100个请求
var tasks = Enumerable.Range(0, 100)
.Select(_ => Task.Delay(10));
await Task.WhenAll(tasks);
}
}
方法耗时线程使用
同步~1000ms1个线程持续占用
异步~10ms线程可以处理其他请求

总结

关键要点
  • 异步编程通过 async/await 提高应用响应性和性能
  • 使用 TaskTask<T> 作为返回类型
  • 避免阻塞异步代码,一路异步到底
  • 使用 Task.WhenAll 并行执行独立任务
  • 在类库中使用 ConfigureAwait(false)
  • 支持取消操作和超时控制

相关资源