C# 异步编程学习笔记
1. 基础语法介绍
async 和 await 的配对关系
async
和 await
是 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
本质上是语法糖,编译器会把异步方法转换成一个状态机:
遇到
await
时,方法会"暂停"并释放当前线程等待的任务完成后,方法从暂停的地方继续执行
整个过程被编译器分解成多个状态,通过状态机管理执行流程
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);
}
对比总结
核心要点回顾:
async/await
是语法糖,本质是状态机异步 ≠ 多线程,别被误导
CancellationToken
让异步操作可控WhenAny/WhenAll
灵活管理多个异步任务yield
创建迭代器,也是状态机IAsyncEnumerable
让异步流式处理成为可能