C#异步编程async/await完全指南


引言

在现代应用程序开发中,异步编程已成为处理I/O密集型操作和提高应用程序响应能力的关键技术。C#通过async/await模式提供了一种简洁而强大的异步编程模型,使开发者能够以接近同步代码的编写方式实现高效的异步操作。本文将全面介绍C#中async/await的工作原理、最佳实践以及常见场景的应用技巧。

一、异步编程基础概念

1.1 同步 vs 异步

  • 同步操作:阻塞当前线程直到操作完成
  • 异步操作:不阻塞当前线程,操作完成后通过回调或通知继续执行

1.2 async/await的优势

  • 代码简洁:避免了回调地狱(Callback Hell)
  • 线程高效:充分利用线程池资源
  • 响应性强:UI线程不会被阻塞
  • 可扩展性好:适合高并发场景

二、async/await核心语法

2.1 基本结构

// 异步方法声明
public async Task<int> GetDataAsync()
{
    // 异步操作
    var data = await DownloadDataAsync();

    // 处理结果
    return ProcessData(data);
}

// 调用异步方法
async Task UseDataAsync()
{
    int result = await GetDataAsync();
    Console.WriteLine(result);
}

2.2 方法签名规则

  • 异步方法必须返回TaskTask<T>ValueTask/ValueTask<T>
  • 方法名通常以”Async”结尾(约定而非强制)
  • 使用async关键字修饰方法

三、底层工作原理

3.1 状态机转换

编译器将async方法转换为状态机:

  1. 遇到第一个await时暂停方法执行
  2. 保存当前上下文(同步上下文、局部变量等)
  3. 返回未完成的Task给调用者
  4. 异步操作完成后恢复执行

3.2 执行流程示例

async Task<string> FetchDataAsync()
{
    Console.WriteLine("开始获取数据"); // 同步部分

    string data = await httpClient.GetStringAsync(url); // 异步等待点

    Console.WriteLine("数据处理"); // 异步部分
    return data.ToUpper();
}

执行顺序:

  1. 输出”开始获取数据”
  2. 发起HTTP请求并返回Task
  3. 方法暂停,控制权返回调用者
  4. HTTP请求完成后,继续执行剩余代码

四、任务(Task)基础

4.1 Task类型

  • Task:表示无返回值的异步操作
  • Task<T>:表示返回T类型结果的异步操作
  • ValueTask/ValueTask<T>:轻量级任务,减少堆分配

4.2 任务控制

// 创建已完成的Task
Task completedTask = Task.CompletedTask;
Task<int> successTask = Task.FromResult(42);

// 组合任务
Task.WhenAll(task1, task2, task3); // 所有任务完成
Task.WhenAny(task1, task2, task3); // 任一任务完成

// 超时控制
await task.TimeoutAfter(TimeSpan.FromSeconds(5));

五、异常处理

5.1 捕获异步异常

try
{
    await SomeAsyncOperation();
}
catch (HttpRequestException ex)
{
    // 处理特定异常
}
catch (Exception ex)
{
    // 处理其他异常
}

5.2 聚合异常处理

Task[] tasks = new Task[3];
try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    if (ex is AggregateException ae)
    {
        foreach (var e in ae.InnerExceptions)
        {
            // 处理每个任务异常
        }
    }
}

六、取消异步操作

6.1 CancellationToken使用

async Task LongRunningOperationAsync(CancellationToken token)
{
    for (int i = 0; i < 100; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100, token);
    }
}

// 调用方
var cts = new CancellationTokenSource();
var task = LongRunningOperationAsync(cts.Token);

// 取消操作
cts.CancelAfter(TimeSpan.FromSeconds(2));

6.2 超时控制

// 使用CancellationToken实现超时
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await SomeAsyncOperation(cts.Token);

// 扩展方法实现
public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
    var cts = new CancellationTokenSource();
    var delayTask = Task.Delay(timeout, cts.Token);

    var completedTask = await Task.WhenAny(task, delayTask);
    if (completedTask == delayTask)
    {
        throw new TimeoutException();
    }

    cts.Cancel();
    await task; // 确保已观察到任何异常
}

七、性能优化技巧

7.1 避免常见陷阱

  • async void:仅用于事件处理程序
  • 阻塞异步代码:避免.Result.Wait()
  • 过度并行:合理控制并发度
  • 虚假异步:确保方法真正异步

7.2 最佳实践

  1. 使用ConfigureAwait(false):库代码中避免同步上下文开销
   var data = await httpClient.GetStringAsync(url).ConfigureAwait(false);
  1. ValueTask优化:对高频调用或可能同步完成的方法
   public ValueTask<int> CacheGetAsync(string key)
   {
       if (cache.TryGetValue(key, out var value))
           return new ValueTask<int>(value);

       return new ValueTask<int>(LoadFromDbAsync(key));
   }
  1. 批处理操作:减少上下文切换
   // 不佳做法
   foreach (var item in items)
   {
       await ProcessItemAsync(item);
   }

   // 优化做法
   var tasks = items.Select(ProcessItemAsync);
   await Task.WhenAll(tasks);

八、高级应用场景

8.1 异步流(Async Streams)

public async IAsyncEnumerable<int> GenerateSequenceAsync()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

// 消费异步流
await foreach (var number in GenerateSequenceAsync())
{
    Console.WriteLine(number);
}

8.2 异步锁

private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1);

async Task AccessSharedResourceAsync()
{
    await _asyncLock.WaitAsync();
    try
    {
        // 访问共享资源
    }
    finally
    {
        _asyncLock.Release();
    }
}

九、与并行编程结合

9.1 并行异步操作

var tasks = uris.Select(uri => httpClient.GetStringAsync(uri));
string[] pages = await Task.WhenAll(tasks);

// 带限流的并行
using var semaphore = new SemaphoreSlim(10);
var tasks = uris.Select(async uri => 
{
    await semaphore.WaitAsync();
    try
    {
        return await httpClient.GetStringAsync(uri);
    }
    finally
    {
        semaphore.Release();
    }
});

9.2 PLINQ与异步

var data = await Task.Run(() => 
    largeCollection.AsParallel()
        .Where(item => item.IsValid)
        .Select(item => item.Value)
        .ToList());

十、实际应用示例

10.1 文件异步读写

async Task<string> ReadFileAsync(string path)
{
    using var reader = File.OpenText(path);
    return await reader.ReadToEndAsync();
}

async Task WriteFileAsync(string path, string content)
{
    using var writer = File.CreateText(path);
    await writer.WriteAsync(content);
}

10.2 数据库异步访问

public async Task<List<Product>> GetProductsAsync(int categoryId)
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();

    var command = new SqlCommand(
        "SELECT * FROM Products WHERE CategoryId = @categoryId", 
        connection);
    command.Parameters.AddWithValue("@categoryId", categoryId);

    await using var reader = await command.ExecuteReaderAsync();

    var products = new List<Product>();
    while (await reader.ReadAsync())
    {
        products.Add(new Product
        {
            Id = reader.GetInt32(0),
            Name = reader.GetString(1),
            Price = reader.GetDecimal(2)
        });
    }

    return products;
}

结语

C#的async/await模式极大地简化了异步编程的复杂性,使开发者能够以直观的方式编写高效的异步代码。通过理解其底层机制、掌握最佳实践并合理应用于各种场景,您可以构建出响应迅速、资源利用率高的应用程序。

记住,异步编程不是万能的——对于CPU密集型任务,传统的多线程或并行编程可能更合适。合理评估应用场景,选择最适合的并发模型,才能真正发挥async/await的优势。随着C#语言的不断发展,异步编程功能也在持续增强,建议保持对最新特性的关注,如C# 10中的异步方法构建器改进等。


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注