C#中的异步编程:async与await

生成模型:QwQ-32B,生成时间:2025-03-20T07:10:00Z

为什么需要异步编程?

  • 同步编程的缺点

    如果直接执行耗时操作(如数据库查询、网络请求),主线程会被阻塞,导致界面无响应或服务器无法处理其他请求。

  • 异步的优势

    异步操作允许在等待 I/O 完成时释放线程,让程序继续执行其他任务,从而提升性能和响应速度。

基础语法

(1) 定义异步方法

  • async 关键字

    修饰方法,表明该方法是 异步的,可以使用 await 关键字。

  • 返回类型

    必须是 TaskTask<T>void(但 async void 一般不推荐,因为无法处理异常和等待)。

public async Task MyAsyncMethod()
{
    // 异步操作代码
}

(2) 使用await等待异步操作

  • await 关键字

    挂起当前方法的执行,直到异步操作完成,然后恢复执行后续代码。

  • 示例

public async Task<int> CalculateAsync()
{
    // 异步等待某个操作(如网络请求)
    var result = await SomeLongRunningTask();
    return result;
}

返回类型

(1) Task

  • 无返回值的异步方法

    如果方法不返回具体值,返回 Task

public async Task DownloadFileAsync()
{
    // 下载文件的异步操作
}

(2) Task<T>

  • 有返回值的异步方法

    返回 Task<T>,其中 T 是结果类型。

public async Task<string> GetAsStringAsync()
{
    // 异步获取字符串
    return await File.ReadAllTextAsync("file.txt");
}

执行流程

  1. 方法开始执行
    当调用 async 方法时,它会立即返回一个 Task 对象(表示异步操作的“句柄”)。
  2. 遇到 await
    当方法中遇到 await 时,当前方法暂停执行,但不会阻塞线程,控制权返回给调用者。
  3. 异步操作完成后
    当异步操作完成,await 后续代码继续执行。
  4. 返回结果
    如果方法返回 Task<T>,最终结果会被包装在 Task<T> 中。

内部机制(状态机)

  • 编译器转换

    编译器会将 async/await 方法转换为一个 状态机,管理异步操作的各个阶段。

  • 简化理解

    你可以将 async/await 看作一种语法糖,底层通过 TaskContinuation 实现异步操作的分步执行。

常见异步方法

(1) Task和Task

  • Task:表示一个无返回值的异步操作。
  • Task<T>:表示一个返回类型为 T 的异步操作。

(2) async方法示例

// 异步方法返回 Task
public async Task DownloadDataAsync()
{
    var data = await GetNetworkDataAsync(); // 等待网络请求
    ProcessData(data); // 处理数据
}

// 异步方法返回 Task<T>
public async Task<string> GetAsStringAsync()
{
    return await File.ReadAllTextAsync("file.txt");
}

错误处理

(1) 使用try-catch

public async Task ProcessAsync()
{
    try
    {
        await SomeMethodThatMayThrowAsync();
    }
    catch (Exception ex)
    {
        // 处理异常
    }
}

(2) 捕获Task的异常

var task = SomeAsyncMethod();
try
{
    await task;
}
catch (Exception ex)
{
    // 处理异常
}

并行执行多个异步操作

(1) 使用Task.WhenAll

public async Task ProcessMultipleAsync()
{
    var task1 = DownloadFileAsync("file1.txt");
    var task2 = DownloadFileAsync("file2.txt");
    
    await Task.WhenAll(task1, task2); // 等待所有任务完成
}

(2) 使用Task.WhenAny

public async Task ProcessFirstCompletedAsync()
{
    var task1 = LongRunningTask1();
    var task2 = LongRunningTask2();
    
    var firstCompleted = await Task.WhenAny(task1, task2);
    if (firstCompleted == task1)
    {
        // 处理 task1 的结果
    }
    else
    {
        // 处理 task2 的结果
    }
}

避免常见错误

(1) 避免async void

  • 错误示例

    async void MyMethod() 无法捕获异常,且无法等待。

  • 正确做法

    返回 TaskTask<T>

(2) 避免阻塞异步代码

  • 错误示例

    // 阻塞主线程!
    var result = myTask.Result; 
    
  • 正确做法

    使用 await

    var result = await myTask;
    

(3) 避免多次await同一个Task

  • 错误示例

    var task = SomeAsyncMethod();
    await task;
    await task; // 第二次 await 会直接返回结果,但可能引发异常
    

实际应用示例

示例 1:文件读取

public async Task<string> ReadFileAsStringAsync()
{
    using (var reader = new StreamReader("file.txt"))
    {
        return await reader.ReadToEndAsync();
    }
}

示例 2:HTTP请求

public async Task<string> GetWebContentAsync(string url)
{
    using (var client = new HttpClient())
    {
        return await client.GetStringAsync(url);
    }
}

示例 3:UI 线程中的异步操作(WPF/WinForms)

private async void Button_Click(object sender, EventArgs e)
{
    // 避免使用 async void,但事件处理可以接受
    var result = await DownloadDataAsync();
    // 更新 UI(确保在主线程)
    textBox.Text = result;
}

关键点总结

  1. async 方法:必须返回 TaskTask<T>void
  2. await 的作用:暂停当前方法,但不阻塞线程,等待异步操作完成。
  3. 错误处理:用 try-catch 捕获异步异常。
  4. 并行任务:用 Task.WhenAllTask.WhenAny 管理多个任务。
  5. 避免阻塞:不要使用 .Result.Wait(),改用 await

进阶知识点

(1) ConfigureAwait

  • 控制异步操作完成后是否返回到原始同步上下文(如 UI 线程)。
var result = await SomeAsync().ConfigureAwait(false); // 不返回原始上下文

(2) CancellationToken

  • 用于取消异步操作。
public async Task DownloadWithCancelAsync(CancellationToken cancellationToken)
{
    await Task.Delay(1000, cancellationToken); // 可被取消
}

:blush: