C# Task async/await CancellationToken
C# Task / async/await / CancellationToken
一、Task
1.Task 理解
Task类似于我们去点餐,餐厅给你一张小票,这个小票就是Task。他表示现在还没有饭,以后会有,你可以去逛街,等饭做好,可以凭借小票(Task)取餐
而在C#
Task=小票
做饭=一个异步操作(可能是读文件、请求网站、查数据库)
逛街=不阻塞当前运行程序,代码可以干其他事。
需要注意的的是Task 不是线程。这是一个最核心也最容易误解的概念。Task 表示的是一项尚未完成的工作或一个未来的结果,它更像一个异步操作的句柄,而不是线程本身
Task<int> task = GetUserCountAsync();
这里的 task 表示用户数量这个结果以后会出来,但完全不等于已经为它开了一个新线程。
Task 的本质有三层含义
- 异步操作的统一抽象: 无论底层是线程池执行计算、操作系统完成异步 I/O、定时器触发,还是回调被包装,最后都可以统一表现为一个Task 或 Task。
- 带有状态: 一个 Task 会经历等待调度、运行中、成功完成、失败或被取消等状态,它还负责承载完成信号、异常、取消状态和 continuation。
- 可组合: Task 可以被组合使用,这是相比传统回调最重要的优势之一。
2. Task 的生命周期
Task 或 Task 具有以下状态(TaskStatus 枚举):
Created:已创建但未启动(仅限使用 new Task() 或 Task.Factory.StartNew 但未调用 Start())。
WaitingForActivation:创建时自动调度(如 Task.Run),等待调度器分配线程。
WaitingToRun:已调度,等待执行。
Running:正在执行。
WaitingForChildrenToComplete:父任务等待子任务完成。
RanToCompletion:完成且无异常(对于 Task,结果可用)。
Canceled:被取消(或触发 OperationCanceledException 但未处理)。
Faulted:发生未处理的异常。
3. 常用 Task 组合器
| 方法 | 作用 | 视觉案例 |
|---|---|---|
| Task.WhenAll | 并行执行多个任务 | 同时处理多台相机的采集 |
| Task.WhenAny | 谁先完成就用谁的结果 | 多个算法模型并行推理,取最快结果 |
| Task.Delay | 非阻塞等待 | 定时轮询设备状态,不影响 UI |
4. 与 Thread 的关系
Thread: 操作系统调度的最小执行单元,属于内核对象。每个线程有自己的栈和寄存器上下文,创建和销毁开销较大。
Task: 一个抽象的工作单元,表示一个异步操作。它由 .NET 的任务并行库(TPL,Task Parallel Library)提供,基于线程池,是对底层线程的更高级封装。
核心关系
- Task 不直接等于 Thread: 一个 Task 可能使用一个线程执行,也可能不使用任何线程(例如纯 I/O 或网络请求完成后触发的回调)。
- Task 运行在线程池上: 默认情况下,Task 由线程池中的线程调度执行。线程池管理一组工作线程,避免频繁创建销毁线程。
- Task 可并发多个: 多个 Task 可以被调度到比 Task 数量少得多的线程上,由线程池负责复用。
主要区别
| 特性 | Thread | Task |
|---|---|---|
| 层级 | 操作系统级 | 语言/运行时级 (.NET) |
| 开销 | 高(1 MB 栈 + 内核对象) | 低(轻量级对象,复用线程) |
| 创建代价 | 昂贵 | 便宜 |
| 控制粒度 | 可手动设置优先级、亲和性 | 通常自动调度,提供 Continuation |
| 等待/组合 | Thread.Join() | Task.Wait() / await、WhenAll、WhenAny |
| 返回值 | 无直接返回值,需共享变量 | 支持返回值和异常传播 (Task) |
| 取消/进度 | 需手动实现 | 内置 CancellationToken、IProgress |
| 适用场景 | 长期阻塞操作、需要显式控制线程参数 | 异步 I/O、并发计算、依赖组合的高层逻辑 |
Task 是逻辑层面的异步操作,Thread 是物理层面的执行资源。
二、async/await 写同步代码一样写异步逻辑
很多人认为 async/await 是某种运行时黑魔法——一个函数加上 async 就能自动变成非阻塞,加上 await 就能让线程“休息”而不会阻塞。实际上,async/await 完全是由编译器在编译期完成的状态机重写。运行时并不知道 async 关键字的存在,它看到的是经过转换的代码。
换句话说,async/await 是 C# 编译器提供的高级语法糖,它自动帮你将异步逻辑切分成多个片段,并在每个 await 点保存/恢复状态。
1. await 的"等待"到底等什么
await 的本质:不阻塞,只是“挂起并返回”
很多人误以为 await task; 会让当前线程阻塞等待 task 完成,这种理解是完全错误的。
await 的行为是:
- 检查 task 是否已经完成(IsCompleted == true)如果是,同步继续,不需要任何额外调度。如果否,进入第 2 步。
- 挂起当前方法:保存方法的状态(局部变量、执行位置等),并立即返回一个未完成的 Task 给调用者。
- 注册一个回调:当 task 完成时,该回调会恢复当前方法的执行(在某个线程上,通常是完成 task 的线程或捕获的同步上下文)。
因此,await 不会阻塞任何线程。这正是异步编程能提高吞吐量的核心:线程可以被释放去做其他工作,而不是白白等待 I/O。
对比阻塞等待
| 方式 | 线程行为 | 适用场景 |
|---|---|---|
| task.Wait() 或 task.Result | 当前线程阻塞,直到 task 完成 | 控制台 Main 方法(有限场景) |
| await task | 当前方法挂起,线程返回调用者,不阻塞 | 几乎所有异步场景 |
如果在 UI 线程(WPF/WinForms)或 ASP.NET Core 请求线程上使用 .Result,轻则降低响应性,重则导致死锁。
何时真的需要阻塞
极少数情况,比如控制台应用的 Main 方法(C# 7.1 之前不支持 async Main),或者某些无法改造为异步的遗留代码。即便如此,更好的做法是使用 await 并让调用链一直异步到入口点。
2. async 关键字的作用
async 关键字本身不创建异步,它只做两件事
- 允许在方法内使用 await(没有 async 就不能用 await)。
- 强制编译器将该方法转换为状态机,并将返回值包装为 Task/Task。
所以 async 更像是一个“标记”,告诉编译器:这个方法体内有异步操作,请帮我生成状态机代码。
避免“异步 void”
public async void Start() → 异常无法捕获,调用方无法等待
public async Task Start(),UI 事件处理程序可以 _ = Start() 或改用 async void 但内部 try-catch
3. 异步方法的返回类型
| 返回类型 | 适用场景 | 说明 |
|---|---|---|
| Task | 没有返回值的异步操作 | 类似 void,但可被 await 和捕获异常 |
| Task | 有返回值的异步操作 | 返回 T 类型的结果 |
| void | 仅限 UI 事件处理程序 | 异步无返回值,但调用方无法等待、无法捕获异常(危险) |
| ValueTask / ValueTask | 高频调用、多数情况同步完成的场景 | 减少堆内存分配,但使用限制较多 |
小Tips:大多数普通应用不需要 ValueTask,使用 Task 更安全。
4. 异常处理
- 在 async 方法内部抛出异常
public async Task<int> DivideAsync(int a, int b){
if (b == 0) throw new DivideByZeroException();
return await Task.FromResult(a / b);
}
调用者用 try/catch 包裹 await 即可捕获异常
try{
int result = await DivideAsync(10, 0);
}catch (DivideByZeroException ex){
// 捕获成功
}
关键点: 异常在 await 处传播,而不是在调用 DivideAsync 时。因为 DivideAsync 返回的 Task 进入 Faulted 状态,await 检测到后会重新抛出异常。
5. async/await 与同步上下文的交互
SynchronizationContext 决定 await 后代码跑在哪个线程
UI 线程(WPF/WinForms):await 后自动回到 UI 线程 → 可直接更新控件
类库 / 后台服务:建议使用 ConfigureAwait(false) 避免不必要的上下文切换
var data = await File.ReadAllTextAsync(path).ConfigureAwait(false);
6. await使用场景
| 场景 | 说明 | 典型API |
|---|---|---|
| 网络请求 | 从远程 API 获取数据,如 REST 调用、下载文件。 | HttpClient.GetStringAsync / PostAsync |
| 文件 I/O | 读写大文件,避免阻塞 UI 或线程池。 | File.ReadAllTextAsync / WriteAsync |
| 数据库操作 | 查询、插入、更新数据(尤其是 ORM 异步方法)。 | SqlCommand.ExecuteReaderAsync DbContext.SaveChangesAsync |
| 延迟或定时 | 模拟等待或实现超时。 | Task.Delay |
| 异步流处理 | 使用 IAsyncEnumerable 逐条消费数据。 | await foreach |
| 并行任务组合 | 同时等待多个异步操作完成。 | Task.WhenAll / WhenAny |
| 跨服务调用 | 微服务间 HTTP/gRPC 调用。 | GrpcClient.XXXAsync |
| 消息队列消费 | 异步接收和处理消息(如 RabbitMQ、Kafka 的异步客户端)。 | BasicConsumeAsync |
三、CancellationToken 取消异步任务
CancellationToken 其实就是一张“可以随时喊停”的凭证。就好比我们的小票,在饭还没开始做的时候可以喊停。(任务在合适的地方取消请求并退出)
CancellationToken 的基本用法
| 角色 | 组件 | 作用 |
|---|---|---|
| 遥控器 | CancellationTokenSource | 产生令牌,并控制取消 |
| 信号线 | CancellationToken | 传递给异步方法,供其监听 |
| 检测开关 | ThrowIfCancellationRequested() | 检测到取消时抛出 OperationCanceledException |
| 检测开关 | IsCancellationRequested | 非侵入式检查,可自己退出循环 |
1. 创建一个CancellationToken 取消程序
//创建“遥控器”和“信号线”
// 1. 创建一个遥控器
CancellationTokenSource cts = new CancellationTokenSource();
// 2. 把信号线交给任务
CancellationToken token = cts.Token;
//写一个可以取消的任务
Task.Run(() =>{
for (int i = 0; i < 100; i++){
// 每次循环都看一眼:有人按取消了吗?
if (token.IsCancellationRequested){
Console.WriteLine("收到取消信号,退出!");
return;
}
// 否则继续干活
Thread.Sleep(100);
}
}, token); // 把 token 传给 Task.Run,让它可以响应取消
// 用户点了取消按钮
cts.Cancel();
另一种更“暴力”的写法
Task.Run(() =>{
for (int i = 0; i < 100; i++){
// 如果取消了,直接抛异常(会被 Task 捕获,任务状态变成 Canceled)
token.ThrowIfCancellationRequested();
// 干活...
}
}, token);
两者区别:
IsCancellationRequested 是你自己判断、自己退出(可以做一些清理工作再 return)。
ThrowIfCancellationRequested 是直接抛异常,让上层 catch 处理,适合不想写一堆 if 的情况。
2. 带超时的取消
// 方法1:使用 CancelAfter
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(200); // 200ms 后自动取消
// 方法2:直接传超时时间
await someTask.WaitAsync(TimeSpan.FromMilliseconds(200));
3. 多个取消信号合并
需要同时响应“用户取消”和“超时自动取消”。可以用 CreateLinkedTokenSource 把两个信号合并。
var userCts = new CancellationTokenSource(); // 用户手动取消
var timeoutCts = new CancellationTokenSource(5000); // 5秒超时
// 把两个遥控器的信号合并成一个
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCts.Token, timeoutCts.Token);
var token = linkedCts.Token;
// 把这个 token 传给任务,无论是用户点取消还是超时,任务都会收到取消信号
await LongRunningTaskAsync(token);
4. 总结
| 我想做什么 | 写什么代码 |
|---|---|
| 创建遥控器 | var cts = new CancellationTokenSource(); |
| 取出信号线 | var token = cts.Token; |
| 把信号线交给任务 | Task.Run(action, token) 或 await xxxAsync(token) |
| 任务内部检查 | if (token.IsCancellationRequested) break; |
| 任务内部抛异常 | token.ThrowIfCancellationRequested(); |
| 触发取消 c | ts.Cancel(); |
| 自动取消(超时) | cts.CancelAfter(1000); |
| 用完释放 | cts.Dispose(); 或 using var cts = new … |
| 合并多个信号 | CancellationTokenSource.CreateLinkedTokenSource(t1, t2) |
练习样例
控制台程序:异步下载 10 张图片,支持中途取消
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
//控制台程序:异步下载 10 张图片,支持中途取消
namespace Console_program_Task_async_await{
class Program{
// 图片URL列表Lorem Picsum 提供的随机图片
private static readonly List<string> ImageUrls = new List<string>{
"https://picsum.photos/id/1/200/300",
"https://picsum.photos/id/2/200/300",
"https://picsum.photos/id/3/200/300",
"https://picsum.photos/id/4/200/300",
"https://picsum.photos/id/5/200/300",
"https://picsum.photos/id/6/200/300",
"https://picsum.photos/id/7/200/300",
"https://picsum.photos/id/8/200/300",
"https://picsum.photos/id/9/200/300",
"https://picsum.photos/id/10/200/300"
};
// 复用 HttpClient
private static readonly HttpClient httpClient = new HttpClient();
static async Task Main(string[] args){
Console.WriteLine("开始下载10张图片...");
Console.WriteLine("提示:按 C 键或按 Ctrl+C 可中途取消下载。\n");
// 创建 CancellationTokenSource 用于取消操作
//CancellationTokenSource 发起取消
//CancellationToken 传递取消请求
using (var cts = new CancellationTokenSource()){
// 注册 Ctrl + C 事件,实现取消
Console.CancelKeyPress += (sender, e) =>{
Console.WriteLine("\n检测到 Ctrl+C,正在取消下载...");
e.Cancel = true; // 阻止进程立即终止
cts.Cancel(); // 触发取消令牌
};
// 创建一个线程任务来监听键盘按键(按 C 键取消)
var keyListenerTask = Task.Run(() =>{
while (!cts.Token.IsCancellationRequested){
if (Console.KeyAvailable){
if (Console.KeyAvailable && char.ToUpper(Console.ReadKey(true).KeyChar) == 'C'){
Console.WriteLine("\n用户按下了 C 键,正在取消下载...");
cts.Cancel();
break;
}
Thread.Sleep(100);
}
}
});
// 创建下载目录
string downloadPath = Path.Combine(Directory.GetCurrentDirectory(), "DownloadedImages");
Directory.CreateDirectory(downloadPath);
try{
//开始下载所有图片
await DownloadAllImagesAsync(ImageUrls, downloadPath, cts.Token);
Console.WriteLine("\n所有图片下载完成!");
}catch (OperationCanceledException){
Console.WriteLine("\n下载已被用户取消。");
} catch (Exception ex){
Console.WriteLine($"\n下载过程中发生错误: {ex.Message}");
}finally{
// 确保键盘监听任务结束
cts.Cancel();
await keyListenerTask;
}
// cts.Dispose() 自动调用
}
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
private static async Task DownloadAllImagesAsync(List<string> urls, string downloadPath, CancellationToken cancellationToken){
var downloadTasks = new List<Task>();
int index = 1;
foreach (string url in urls){
string filePath = Path.Combine(downloadPath, $"image_{index++}.jpg");
downloadTasks.Add(DownloadImageAsync(url, filePath, index, cancellationToken));
}
await Task.WhenAll(downloadTasks);
}
// 下载单张图片并保存到本地
private static async Task DownloadImageAsync(string url, string filePath, int imageIndex, CancellationToken cancellationToken){
try{
Console.WriteLine($"任务 {imageIndex} 开始下载:{url}");
using (HttpResponseMessage response = await httpClient.GetAsync(url, cancellationToken)){
//检查 HTTP 响应的状态码是否为成功(2xx),如果不是,则抛出一个异常。
response.EnsureSuccessStatusCode(); // 确保请求成功
Console.WriteLine(response.ToString());
Console.WriteLine("==================");
byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
// 异步写入文件(支持取消)
using (var fileStream = new FileStream(
filePath, // 1. 文件路径
FileMode.Create, // 2. 文件模式(创建/覆盖)
FileAccess.Write, // 3. 文件访问权限(只写)
FileShare.None, // 4. 共享模式(不共享)
4096, // 5. 缓冲区大小(字节)
useAsync: true)) // 6. 是否使用异步 I/O
{
await fileStream.WriteAsync(
imageBytes, // 要写入的数据源
0, // 从 imageBytes 的哪个索引开始写
imageBytes.Length, // 要写入的字节数
cancellationToken);// 取消令牌
}
}
Console.WriteLine($"[任务 {imageIndex}] 下载完成并保存至:{filePath}");
}catch (OperationCanceledException){
Console.WriteLine($"[任务 {imageIndex}] 下载已取消。");
throw;
}catch (Exception ex){
Console.WriteLine($"[任务 {imageIndex}] 下载失败:{ex.Message}");
throw;
}
}
}
}
本文参考:
C# 官方文档https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-create-pre-computed-tasks
博客园 https://www.cnblogs.com/yilezhu/p/10555849.html
Deepseek https://chat.deepseek.com/
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)