Decade
Decade
Published on 2025-07-24 / 42 Visits
0
0

异步方法

C# 异步编程学习笔记

1. 基础语法介绍

async 和 await 的配对关系

asyncawait 是 C# 中异步编程的核心关键字,它们总是配对出现:

  • async 修饰符:标记一个方法为异步方法,告诉编译器"这个方法里面会有异步操作"

  • await 操作符:等待一个异步操作完成,"我在这里等一下,等这个任务完成了再继续"

// async 标记这是一个异步方法
public async Task<string> GetDataAsync()
{
    HttpClient client = new HttpClient();
    // await 等待网络请求完成
    string result = await client.GetStringAsync("https://api.example.com/data");
    return result;
}

工作原理

async/await 本质上是语法糖,编译器会把异步方法转换成一个状态机

  1. 遇到 await 时,方法会"暂停"并释放当前线程

  2. 等待的任务完成后,方法从暂停的地方继续执行

  3. 整个过程被编译器分解成多个状态,通过状态机管理执行流程

public async Task DemoAsync()
{
    Console.WriteLine("开始执行");           // 状态 0
    
    await Task.Delay(1000);                 // 状态 1:暂停,等待1秒
    
    Console.WriteLine("1秒后继续");          // 状态 2:恢复执行
    
    await Task.Delay(2000);                 // 状态 3:再次暂停
    
    Console.WriteLine("又过了2秒");          // 状态 4:完成
}

2. 异步不等于多线程

这是一个常见误区:异步方法不一定在新线程上运行

同一线程的异步操作

public async Task SameThreadDemo()
{
    Console.WriteLine($"方法开始 - 线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    // 这个异步操作可能还在同一线程执行
    await Task.Yield();
    
    Console.WriteLine($"继续执行 - 线程ID: {Thread.CurrentThread.ManagedThreadId}");
    // 很可能显示相同的线程ID
}

显式使用多线程

只有显式创建新线程时,才会真正的多线程执行:

public async Task MultiThreadDemo()
{
    Console.WriteLine($"主线程ID: {Thread.CurrentThread.ManagedThreadId}");
    
    // Task.Run 会在线程池中开启新线程
    await Task.Run(() =>
    {
        Console.WriteLine($"新线程ID: {Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(1000);
    });
    
    Console.WriteLine("回到主线程继续");
}

重点理解:异步主要解决的是 I/O 等待问题(如网络请求、文件读写),让线程不必傻等,而不是为了并行计算。

3. CancellationToken

为什么需要取消机制

想象这样的场景:

  • 用户在搜索框输入,每次输入都发起搜索请求

  • 用户快速输入时,前面的请求还没完成,但已经没用了

  • 这时需要取消之前的请求,避免浪费资源

基本用法

public async Task CancellableOperation()
{
    // 创建取消令牌源
    CancellationTokenSource cts = new CancellationTokenSource();
    
    // 模拟用户5秒后取消操作
    cts.CancelAfter(5000);
    
    try
    {
        await LongRunningTask(cts.Token);
        Console.WriteLine("任务完成");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("任务被取消了");
    }
}

public async Task LongRunningTask(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        // 检查是否收到取消请求
        token.ThrowIfCancellationRequested();
        
        Console.WriteLine($"正在处理第 {i + 1} 步...");
        await Task.Delay(1000, token);  // Delay 也支持取消
    }
}

实际应用示例

public async Task<string> SearchWithTimeout(string keyword)
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
    {
        try
        {
            return await SearchAsync(keyword, cts.Token);
        }
        catch (OperationCanceledException)
        {
            return "搜索超时,请重试";
        }
    }
}

4. Task.WhenAny / Task.WhenAll

Task.WhenAny - 竞速模式

多个任务同时进行,只要有一个完成就继续

public async Task<string> GetFastestResponse()
{
    // 同时请求多个服务器
    Task<string> server1 = GetFromServer1Async();
    Task<string> server2 = GetFromServer2Async();
    Task<string> server3 = GetFromServer3Async();
    
    // 哪个最快完成就用哪个
    Task<string> firstCompleted = await Task.WhenAny(server1, server2, server3);
    string result = await firstCompleted;  // 注意:需要再次 await 获取结果
    
    return result;
}

Task.WhenAll - 等待所有

必须等待所有任务完成

public async Task ProcessMultipleFiles()
{
    string[] files = { "file1.txt", "file2.txt", "file3.txt" };
    
    // 创建所有任务
    Task[] tasks = files.Select(file => ProcessFileAsync(file)).ToArray();
    
    // 等待全部完成
    await Task.WhenAll(tasks);
    Console.WriteLine("所有文件处理完成");
}

// 带返回值的版本
public async Task<int> CalculateTotalAsync()
{
    Task<int> task1 = GetCount1Async();
    Task<int> task2 = GetCount2Async();
    Task<int> task3 = GetCount3Async();
    
    int[] results = await Task.WhenAll(task1, task2, task3);
    return results.Sum();  // 所有结果的总和
}

5. yield 与状态机

yield 关键字的本质也是状态机,用于创建迭代器:

基本示例

public IEnumerable<int> GenerateNumbers()
{
    Console.WriteLine("开始生成数字");
    
    yield return 1;  // 暂停,返回 1
    Console.WriteLine("生成了1");
    
    yield return 2;  // 暂停,返回 2
    Console.WriteLine("生成了2");
    
    yield return 3;  // 暂停,返回 3
    Console.WriteLine("生成了3");
}

// 使用方式
foreach (int number in GenerateNumbers())
{
    Console.WriteLine($"获取到: {number}");
}

/* 输出:
开始生成数字
获取到: 1
生成了1
获取到: 2
生成了2
获取到: 3
生成了3
*/

延迟执行的特性

public IEnumerable<string> ReadLargeFile(string path)
{
    using (var reader = new StreamReader(path))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;  // 一次只读一行,不会把整个文件加载到内存
        }
    }
}

6. IAsyncEnumerable 与 async/await 结合

C# 8.0 引入了 IAsyncEnumerable<T>,让异步和迭代完美结合。

传统方式的问题

// 传统方式:必须等待所有数据加载完成
public async Task<IEnumerable<string>> GetAllDataAsync()
{
    await Task.Delay(5000);  // 模拟获取所有数据需要5秒
    return new[] { "data1", "data2", "data3" };
}

// 使用时必须等待全部完成
var allData = await GetAllDataAsync();  // 等5秒
foreach (var item in allData)
{
    Console.WriteLine(item);
}

使用 IAsyncEnumerable 的优势

// 新方式:边获取边处理
public async IAsyncEnumerable<string> GetDataStreamAsync()
{
    for (int i = 1; i <= 3; i++)
    {
        await Task.Delay(1000);  // 模拟每个数据需要1秒
        yield return $"data{i}";  // 立即返回这一条
    }
}

// 使用 await foreach
await foreach (var item in GetDataStreamAsync())
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss} - 收到: {item}");
    // 每秒输出一条,而不是等3秒后一次性输出
}

实际应用场景

public async IAsyncEnumerable<WeatherData> GetRealtimeWeatherAsync(
    [EnumeratorCancellation] CancellationToken token = default)
{
    while (!token.IsCancellationRequested)
    {
        var weather = await FetchCurrentWeatherAsync();
        yield return weather;
        
        await Task.Delay(60000, token);  // 每分钟更新一次
    }
}

// 实时显示天气
await foreach (var weather in GetRealtimeWeatherAsync())
{
    UpdateWeatherDisplay(weather);
}

对比总结

特性

Task<IEnumerable<T>>

IAsyncEnumerable<T>

等待时间

必须等所有数据就绪

可以逐个处理

内存占用

一次性加载全部

按需加载

用户体验

长时间等待

立即响应

适用场景

数据量小且固定

流式数据、实时数据


核心要点回顾

  1. async/await 是语法糖,本质是状态机

  2. 异步 ≠ 多线程,别被误导

  3. CancellationToken 让异步操作可控

  4. WhenAny/WhenAll 灵活管理多个异步任务

  5. yield 创建迭代器,也是状态机

  6. IAsyncEnumerable 让异步流式处理成为可能


Comment