C# Task / async/await / CancellationToken

一、Task

1.Task 理解

Task类似于我们去点餐,餐厅给你一张小票,这个小票就是Task。他表示现在还没有饭,以后会有,你可以去逛街,等饭做好,可以凭借小票(Task)取餐

而在C#
Task=小票
做饭=一个异步操作(可能是读文件、请求网站、查数据库)
逛街=不阻塞当前运行程序,代码可以干其他事。

需要注意的的是Task 不是线程。这是一个最核心也最容易误解的概念。Task 表示的是一项尚未完成的工作一个未来的结果,它更像一个异步操作的句柄,而不是线程本身

Task<int> task = GetUserCountAsync();

这里的 task 表示用户数量这个结果以后会出来,但完全不等于已经为它开了一个新线程。

Task 的本质有三层含义

  1. 异步操作的统一抽象: 无论底层是线程池执行计算、操作系统完成异步 I/O、定时器触发,还是回调被包装,最后都可以统一表现为一个Task 或 Task。
  2. 带有状态: 一个 Task 会经历等待调度、运行中、成功完成、失败或被取消等状态,它还负责承载完成信号、异常、取消状态和 continuation。
  3. 可组合: 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)提供,基于线程池,是对底层线程的更高级封装。

核心关系

  1. Task 不直接等于 Thread: 一个 Task 可能使用一个线程执行,也可能不使用任何线程(例如纯 I/O 或网络请求完成后触发的回调)。
  2. Task 运行在线程池上: 默认情况下,Task 由线程池中的线程调度执行。线程池管理一组工作线程,避免频繁创建销毁线程。
  3. 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 的行为是:

  1. 检查 task 是否已经完成(IsCompleted == true)如果是,同步继续,不需要任何额外调度。如果否,进入第 2 步。
  2. 挂起当前方法:保存方法的状态(局部变量、执行位置等),并立即返回一个未完成的 Task 给调用者。
  3. 注册一个回调:当 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 关键字本身不创建异步,它只做两件事

  1. 允许在方法内使用 await(没有 async 就不能用 await)。
  2. 强制编译器将该方法转换为状态机,并将返回值包装为 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. 异常处理

  1. 在 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/

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐