C#.Net-多线程-Async-Await篇-学习笔记
一、async/await 基础
1.1 什么是async/await
定义
- C# 5.0 (.NET 4.5) 引入的语法糖
- C# 7.1 开始,Main入口也可以使用
- C# 8.0 支持异步流(await foreach)和异步释放(await using)
什么是语法糖
- 由编译器提供的便捷功能
- 底层实现不变,但写代码更简洁
- 类似的语法糖:var、表达式树、属性的get/set、字符串插值$""
1.2 基本用法
规则
csharp
// 1. async单独使用会警告,没有实际作用
public async void Method1() // 警告
{
Console.WriteLine("没有await");
}
// 2. await必须在async方法内使用
public void Method2()
{
await Task.Delay(1000); // 编译错误
}
// 3. async + await 配套使用
public async Task Method3()
{
await Task.Delay(1000); // 正确
}
返回值规则
csharp
// 无返回值:返回Task(推荐)或void
public async Task NoReturnAsync()
{
await Task.Delay(1000);
// 默认返回Task
}
public async void NoReturnVoid()
{
await Task.Delay(1000);
// 不推荐,无法await或Wait
}
// 有返回值:返回Task<T>
public async Task<int> GetValueAsync()
{
await Task.Delay(1000);
return 123; // 实际返回Task<int>
}
为什么推荐Task而不是void
- Task可以使用await、Wait、WhenAny、WhenAll等方法组合
- void无法组合使用
- Task可以捕获异常,void不行
二、async/await的核心价值
2.1 解决的问题
问题:既要有顺序,又要不阻塞
csharp
// 同步方法:有顺序,但阻塞
public void SyncMethod()
{
Console.WriteLine("步骤1");
Thread.Sleep(1000); // 阻塞
Console.WriteLine("步骤2");
Thread.Sleep(1000); // 阻塞
Console.WriteLine("步骤3");
}
// 多线程:不阻塞,但无顺序
public void TaskMethod()
{
Task.Run(() =>
{
Console.WriteLine("步骤1");
Thread.Sleep(1000);
});
Task.Run(() =>
{
Console.WriteLine("步骤2");
Thread.Sleep(1000);
});
Task.Run(() =>
{
Console.WriteLine("步骤3");
Thread.Sleep(1000);
});
// 输出顺序不确定
}
// async/await:有顺序,不阻塞
public async Task AsyncMethod()
{
await Task.Run(() =>
{
Console.WriteLine("步骤1");
Thread.Sleep(1000);
});
await Task.Run(() =>
{
Console.WriteLine("步骤2");
Thread.Sleep(1000);
});
await Task.Run(() =>
{
Console.WriteLine("步骤3");
Thread.Sleep(1000);
});
// 按顺序输出:步骤1 -> 步骤2 -> 步骤3
}
2.2 核心理念
以同步编程的方式来写异步代码
- 代码看起来像同步,实际是异步执行
- 降低编程难度
- 保持代码可读性
三、await的执行机制
3.1 执行流程
csharp
public async Task TestAsync()
{
Console.WriteLine($"主线程ID: {Thread.CurrentThread.ManagedThreadId}");
// 1. 遇到await,主线程立即返回(不阻塞)
await Task.Run(() =>
{
Console.WriteLine($"子线程ID: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(3000);
Console.WriteLine("子线程完成");
});
// 2. await后的代码,由线程池的线程执行
Console.WriteLine($"await后线程ID: {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("继续执行");
}
关键点
- 主线程遇到 await 就返回,不等待(非阻塞)
- await 后的代码会在 Task 完成后继续执行
- await 后的代码可能由子线程、主线程或其他线程执行
一个典型的执行顺序示例,能直观说明 await 的非阻塞特性:
csharp
public static void Show()
{
Console.WriteLine($"start 线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ① 主线程
Async(); // 调用异步方法
Console.WriteLine($"aaa 线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ③ 主线程继续,不等待
}
public static async void Async()
{
Console.WriteLine($"ddd 线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ② 还是主线程
await Task.Run(() =>
{
Thread.Sleep(500);
Console.WriteLine($"bbb 线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ④ 子线程
});
Console.WriteLine($"ccc 线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ⑤ await后,线程池线程
}
实际输出顺序:start → ddd → aaa → bbb → ccc
⚠️ 注意 aaa 在 bbb 之前输出------主线程遇到 await 后立即返回继续执行,子线程的工作和主线程是并发的。
3.2 与ContinueWith的对比
async/await 出现之前,控制多个异步操作的顺序只能靠 ContinueWith 链式回调,代码可读性很差:
csharp
// 老写法:ContinueWith 链式调用控制顺序,嵌套深、难维护
Task<int> task = taskFactory.StartNew<int>(() =>
{
Thread.Sleep(3000);
return 123;
}).ContinueWith(c =>
{
Thread.Sleep(3000);
return 234;
}).ContinueWith(t =>
{
Thread.Sleep(3000);
return 345;
});
用 await 改写后,代码结构和同步方法一样直观:
csharp
// 新写法:await 顺序执行,清晰易读
public async Task UseAwait()
{
await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("步骤1"); });
await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("步骤2"); });
await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("步骤3"); });
}
await 的优势
- 代码更简洁,可读性更好
- 写法像同步代码,实际是异步执行
- 自动处理异常传播,不需要手动在每个 ContinueWith 里 try-catch
四、带返回值的async方法
4.1 基本用法
csharp
public async Task<long> SumAsync()
{
Console.WriteLine($"开始计算,线程ID: {Thread.CurrentThread.ManagedThreadId}");
long result = 0;
// 第一个计算
await Task.Run(() =>
{
Console.WriteLine($"计算1,线程ID: {Thread.CurrentThread.ManagedThreadId}");
for (long i = 0; i < 999_999_999; i++)
{
result += i;
}
});
Console.WriteLine($"计算1完成,线程ID: {Thread.CurrentThread.ManagedThreadId}");
// 第二个计算
await Task.Run(() =>
{
Console.WriteLine($"计算2,线程ID: {Thread.CurrentThread.ManagedThreadId}");
for (long i = 0; i < 999_999_999; i++)
{
result += i;
}
});
Console.WriteLine($"计算2完成,线程ID: {Thread.CurrentThread.ManagedThreadId}");
return result; // 自动包装成Task<long>
}
4.2 调用方式
csharp
// 方式1:await(推荐,不阻塞)
public async Task CallAsync()
{
long result = await SumAsync();
Console.WriteLine($"结果: {result}");
}
// 方式2:Result(不推荐,阻塞)
public void CallSync()
{
long result = SumAsync().Result; // 阻塞,相当于同步
Console.WriteLine($"结果: {result}");
}
// 方式3:Wait(不推荐,阻塞)
public void CallWait()
{
Task<long> task = SumAsync();
task.Wait(); // 阻塞
long result = task.Result;
Console.WriteLine($"结果: {result}");
}
注意
- 访问Result或Wait会阻塞,失去async/await的意义
- 推荐使用await,保持异步特性
五、async/await的底层实现
5.1 状态机
IL代码分析
csharp
// C#代码
public async Task SimpleMethod()
{
await Task.Delay(1000);
Console.WriteLine("完成");
}
// 编译后生成状态机(简化版)
private struct StateMachine
{
public int state;
public AsyncTaskMethodBuilder builder;
public void MoveNext()
{
try
{
switch (state)
{
case 0:
// 执行await之前的代码
var awaiter = Task.Delay(1000).GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
// 等待完成
return;
}
goto case 1;
case 1:
// 执行await之后的代码
Console.WriteLine("完成");
state = -2;
break;
}
}
catch (Exception ex)
{
state = -2;
builder.SetException(ex);
}
builder.SetResult();
}
}
状态机执行流程
- 实例化状态机
- 将状态机交给builder执行
- 整理线程上下文
- 调用MoveNext()方法
- 根据状态执行不同分支
- 异常时状态重置为-2
- 完成时调用SetResult()
5.2 状态机的价值
类比:红绿灯
- 红灯:停止
- 绿灯:行驶
- 黄灯:减速
状态机
- 一个对象在不同状态下执行不同行为
- await将方法分割成多个状态
- 每个状态对应一个await
六、性能对比
6.1 文件读取对比
csharp
// 1. Async版本(推荐)
private async Task<byte[]> ReadAsync(string path)
{
Console.WriteLine($"ReadAsync开始,线程ID: {Thread.CurrentThread.ManagedThreadId}");
var result = await File.ReadAllBytesAsync(path);
// 读取文件时,没有开启新线程
// 主线程告诉系统要做什么,然后就返回了
// 降低了线程开启数量,降低了CPU负荷
Console.WriteLine($"ReadAsync结束,线程ID: {Thread.CurrentThread.ManagedThreadId}");
return result;
}
// 2. Task版本
private Task<byte[]> ReadTask(string path)
{
Console.WriteLine($"ReadTask开始,线程ID: {Thread.CurrentThread.ManagedThreadId}");
var result = Task.Run(() =>
{
Console.WriteLine($"ReadTask执行,线程ID: {Thread.CurrentThread.ManagedThreadId}");
return File.ReadAllBytes(path);
});
// 铁定开启新线程
Console.WriteLine($"ReadTask结束,线程ID: {Thread.CurrentThread.ManagedThreadId}");
return result;
}
// 3. Sync版本
private byte[] ReadSync(string path)
{
Console.WriteLine($"ReadSync开始,线程ID: {Thread.CurrentThread.ManagedThreadId}");
var result = File.ReadAllBytes(path);
Console.WriteLine($"ReadSync结束,线程ID: {Thread.CurrentThread.ManagedThreadId}");
return result;
}
性能测试结果
20次读取大文件:
- Async: 约3000ms
- Task: 约3500ms
- Sync: 约6000ms
6.2 Web请求对比
csharp
// 1. Async版本(推荐)
private async Task<string> WebAsync(string url)
{
HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
// 使用异步版本的API
using (HttpWebResponse response = await request.GetResponseAsync() as HttpWebResponse)
{
StreamReader sr = new StreamReader(response.GetResponseStream());
return await sr.ReadToEndAsync(); // 异步读取
}
}
// 2. Task版本
private Task<string> WebTask(string url)
{
return Task.Run(() =>
{
return InvokeWebRequest(url); // 同步方法包装
});
}
// 3. Sync版本
private string WebSync(string url)
{
return InvokeWebRequest(url);
}
性能测试结果
10次Web请求(每次5秒):
- Async: 约5秒(并发)
- Task: 约5秒(并发)
- Sync: 约50秒(串行)
6.3 CPU密集型计算对比
csharp
// 1. Async版本
private async Task<long> CalculationAsync(long total)
{
return await Task.Run(() =>
{
long result = 0;
for (long i = 0; i < total; i++)
{
result += i;
}
return result;
});
}
// 2. Task版本
private Task<long> CalculationTask(long total)
{
return Task.Run(() =>
{
long result = 0;
for (long i = 0; i < total; i++)
{
result += i;
}
return result;
});
}
// 3. Sync版本
private long CalculationSync(long total)
{
long result = 0;
for (long i = 0; i < total; i++)
{
result += i;
}
return result;
}
性能测试结果
10次计算(每次10亿次循环):
- Task: 约8秒
- Async: 约8秒
- Sync: 约80秒
结论
- CPU密集型:Async和Task性能相同,都需要开启线程
- IO密集型:Async性能更好,不需要额外线程
七、适用场景
7.1 适合使用async/await的场景
1. IO密集型操作(推荐)
csharp
// 文件操作
await File.ReadAllBytesAsync(path);
await File.WriteAllTextAsync(path, content);
// 数据库操作
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
// Web请求
await httpClient.GetStringAsync(url);
// Redis操作
await redis.StringGetAsync(key);
2. 与第三方交互(非托管资源)
- 数据库查询
- Redis缓存
- Web API调用
- 文件读写
7.2 不适合使用async/await的场景
1. CPU密集型计算(不推荐)
csharp
// 不推荐:async/await对CPU密集型无优势
public async Task<long> CalculateAsync()
{
return await Task.Run(() =>
{
// 大量计算
long result = 0;
for (long i = 0; i < 1_000_000_000; i++)
{
result += i;
}
return result;
});
}
// 推荐:直接使用Task
public Task<long> CalculateTask()
{
return Task.Run(() =>
{
long result = 0;
for (long i = 0; i < 1_000_000_000; i++)
{
result += i;
}
return result;
});
}
2. 本地计算(托管资源)
- 内存中的数据处理
- 算法计算
- 数据转换
八、不同框架中的应用
8.1 ASP.NET Core(推荐使用)
csharp
[HttpGet]
public async Task<IActionResult> GetDataAsync()
{
var data = await _service.GetDataAsync();
return Ok(data);
}
优势
- 不阻塞线程池线程
- 提高服务器并发能力
- 可以处理更多请求
8.2 控制台应用(可以使用)
csharp
// C# 7.1+
static async Task Main(string[] args)
{
await DoWorkAsync();
}
8.3 WinForms(需要注意)
csharp
private async void btnClick_Click(object sender, EventArgs e)
{
// 不阻塞UI线程
var result = await GetDataAsync();
// await后的代码在UI线程执行,可以直接更新UI
lblResult.Text = result;
}
注意事项
- 事件处理器可以使用async void
- await后的代码会回到UI线程
- 可以直接更新UI控件
8.4 WPF(类似WinForms)
csharp
private async void Button_Click(object sender, RoutedEventArgs e)
{
var result = await GetDataAsync();
TextBlock.Text = result;
}
九、C# 8.0 新特性
9.1 异步流(await foreach)
csharp
// 生成异步序列
public async IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}
// 消费异步序列
private async Task ConsumeAsync()
{
await foreach (var i in GenerateSequence())
{
Console.WriteLine($"接收到: {i}");
}
}
9.2 异步释放(await using)
csharp
await using (var resource = new AsyncDisposableResource())
{
// 使用资源
}
// 自动调用DisposeAsync()
十、最佳实践
10.1 命名规范
csharp
// 异步方法以Async结尾
public async Task<string> GetDataAsync()
{
return await _repository.QueryAsync();
}
10.2 避免async void
csharp
// 错误:无法捕获异常,无法await
public async void BadMethodAsync()
{
await Task.Delay(1000);
}
// 正确:返回Task
public async Task GoodMethodAsync()
{
await Task.Delay(1000);
}
// 例外:事件处理器可以使用async void
private async void Button_Click(object sender, EventArgs e)
{
await DoWorkAsync();
}
10.3 避免阻塞
csharp
// 错误:阻塞
var result = GetDataAsync().Result;
// 错误:阻塞
GetDataAsync().Wait();
// 正确:异步等待
var result = await GetDataAsync();
10.4 ConfigureAwait
csharp
// 库代码中使用,避免捕获上下文
var result = await GetDataAsync().ConfigureAwait(false);
// UI代码中不使用,需要回到UI线程
var result = await GetDataAsync();
十一、小结
- async/await 是语法糖:底层是状态机实现
- 核心价值:以同步方式写异步,降低编程难度
- 适用场景:IO 密集型操作(文件、网络、数据库)
- 不适用场景:CPU 密集型计算
- 性能提升:提高吞吐量,不是降低单个请求时间
- 避免阻塞:不要使用 Result 或 Wait
- 命名规范:异步方法以 Async 结尾
- 返回类型:优先使用 Task,避免 void