C# 取消机制(CancellationTokenSource/CancellationToken)

C# 取消机制

什么是取消机制?

想象一下这个场景:你在网上下载一个大文件,突然发现下错了,点击"取消"按钮。你期望的是下载立即停止,而不是程序卡死或者继续在后台下载完。

C# 的取消机制就是解决这个问题的!它是一种协作式的取消方式,意思是:

  • 不是强制终止:不像强行关闭程序那样粗暴

  • 礼貌地请求停止:向正在执行的任务发送"请停止"的信号

  • 任务自己决定如何停止:任务收到信号后,可以安全地保存状态、释放资源,然后优雅退出


创建和基本使用

cs 复制代码
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        // 创建 CancellationTokenSource
        var cts = new CancellationTokenSource();
        
        // 启动一个可取消的任务
        Task.Run(() => DoWork(cts.Token));
        
        // 等待用户输入后取消
        Console.ReadLine();
        cts.Cancel();  // 取消
        
        Console.WriteLine("取消信号已发送");
        Console.ReadLine();
    }
    
    static void DoWork(CancellationToken cancellationToken)
    {
        try
        {
            for (int i = 0; i < 100; i++)
            {
                // 检查取消请求
                cancellationToken.ThrowIfCancellationRequested();
                
                Console.WriteLine($"工作进度: {i}%");
                Thread.Sleep(100);
            }
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("操作已被取消");
        }
    }
}

完整的生活化例子

让我们用一个更贴近生活的例子来解释:

场景:厨房做饭

cs 复制代码
using System;
using System.Threading;
using System.Threading.Tasks;

class Kitchen
{
    static async Task Main()
    {
        // 厨师开始做饭
        var chef = new Chef();
        
        // 客人有一个"取消按钮"(如果等不及可以取消订单)
        var guestCancellation = new CancellationTokenSource();
        
        // 厨房也有一个超时限制(30分钟没做完自动取消)
        var kitchenTimeout = new CancellationTokenSource(TimeSpan.FromMinutes(30));
        
        // 组合两个取消源:客人取消 OR 厨房超时
        using var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(
            guestCancellation.Token, kitchenTimeout.Token);
        
        try
        {
            // 开始做饭!
            Console.WriteLine("厨师开始准备餐点...");
            await chef.CookMealAsync(combinedCancellation.Token);
            Console.WriteLine("餐点准备完成!");
        }
        catch (OperationCanceledException) when (guestCancellation.Token.IsCancellationRequested)
        {
            Console.WriteLine("客人取消了订单");
        }
        catch (OperationCanceledException) when (kitchenTimeout.Token.IsCancellationRequested)
        {
            Console.WriteLine("厨房超时,无法完成订单");
        }
        
        // 模拟客人取消
        Console.WriteLine("\n按回车键模拟客人取消订单...");
        Console.ReadLine();
        guestCancellation.Cancel(); // 取消
    }
}

class Chef
{
    public async Task CookMealAsync(CancellationToken cancellationToken)
    {
        string[] steps = { "准备食材", "切菜", "烹饪", "摆盘", "上菜" };
        
        foreach (var step in steps)
        {
            // 检查是否收到取消信号
            cancellationToken.ThrowIfCancellationRequested();
            
            Console.WriteLine($"正在: {step}");
            await Task.Delay(2000, cancellationToken); // 模拟每个步骤需要2秒
        }
    }
}

核心类详解

1. CancellationTokenSource 类

这是取消操作的发起者,负责创建和管理取消信号。

构造函数
cs 复制代码
// 1. 默认构造函数 - 创建未取消的源
var cts1 = new CancellationTokenSource();

// 2. 带超时的构造函数 - 指定时间后自动取消
var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5秒后取消
var cts3 = new CancellationTokenSource(5000); // 5000毫秒后取消
主要属性
cs 复制代码
public class CancellationTokenSourceProperties
{
    public static void DemonstrateProperties()
    {
        var cts = new CancellationTokenSource();
        
        // Token: 获取与此源关联的 CancellationToken
        CancellationToken token = cts.Token;
        Console.WriteLine($"Token: {token}");
        
        // IsCancellationRequested: 检查是否已请求取消
        bool isCancelled = cts.IsCancellationRequested;
        Console.WriteLine($"是否已取消: {isCancelled}"); // 初始为 false
    }
}
主要方法
cs 复制代码
public class CancellationTokenSourceMethods
{
    public static void DemonstrateMethods()
    {
        var cts = new CancellationTokenSource();
        
        // Cancel(): 请求取消操作
        Console.WriteLine("调用 Cancel() 方法...");
        cts.Cancel();
        Console.WriteLine($"取消状态: {cts.IsCancellationRequested}"); // true
        
        // CancelAfter(): 在指定时间后自动取消
        var delayedCts = new CancellationTokenSource();
        delayedCts.CancelAfter(TimeSpan.FromSeconds(3)); // 3秒后自动取消
        Console.WriteLine($"3秒后将自动取消: {delayedCts.IsCancellationRequested}"); // false
        
        // Dispose(): 释放资源
        cts.Dispose();
        delayedCts.Dispose();
    }
}
静态方法
cs 复制代码
public class CancellationTokenSourceStaticMethods
{
    public static void DemonstrateStaticMethods()
    {
        var cts1 = new CancellationTokenSource();
        var cts2 = new CancellationTokenSource();
        
        // CreateLinkedTokenSource(): 创建链接的取消源
        // 当任意一个源取消时,链接的源也会取消
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            cts1.Token, cts2.Token);
        
        Console.WriteLine("当 cts1 或 cts2 取消时,linkedCts 也会自动取消");
        
        // 测试链接效果
        cts1.Cancel();
        Console.WriteLine($"cts1 取消状态: {cts1.IsCancellationRequested}"); // true
        Console.WriteLine($"linkedCts 取消状态: {linkedCts.IsCancellationRequested}"); // true
    }
}

2. CancellationToken 结构

这是取消操作的接收者,用于检测取消请求。

主要属性
cs 复制代码
public class CancellationTokenProperties
{
    public static void DemonstrateProperties(CancellationToken token)
    {
        // IsCancellationRequested: 是否已请求取消
        Console.WriteLine($"是否请求取消: {token.IsCancellationRequested}");
        
        // CanBeCanceled: 此令牌能否被取消
        Console.WriteLine($"能否被取消: {token.CanBeCanceled}");
        
        // WaitHandle: 获取等待取消信号的可等待句柄
        Console.WriteLine($"WaitHandle: {token.WaitHandle}");
        
        // None: 静态属性,返回空的无法取消的令牌
        var emptyToken = CancellationToken.None;
        Console.WriteLine($"空令牌能否取消: {emptyToken.CanBeCanceled}"); // false
    }
}
主要方法
cs 复制代码
public class CancellationTokenMethods
{
    public static void DemonstrateMethods()
    {
        var cts = new CancellationTokenSource();
        var token = cts.Token;
        
        // ThrowIfCancellationRequested(): 如果已请求取消,则抛出 OperationCanceledException
        try
        {
            token.ThrowIfCancellationRequested(); // 此时不会抛出异常
            Console.WriteLine("没有取消请求,继续执行...");
            
            cts.Cancel(); // 请求取消
            token.ThrowIfCancellationRequested(); // 这里会抛出异常
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("捕获到 OperationCanceledException");
        }
        
        // Register(): 注册取消时的回调函数
        var callbackCts = new CancellationTokenSource();
        var callbackToken = callbackCts.Token;
        
        // 注册回调 - 当取消时自动执行
        CancellationTokenRegistration registration = 
            callbackToken.Register(() => 
            {
                Console.WriteLine("取消回调被执行!");
                Console.WriteLine("执行清理操作...");
            });
        
        Console.WriteLine("注册了取消回调");
        callbackCts.Cancel(); // 触发回调
        
        // 可以取消注册
        registration.Dispose();
        Console.WriteLine("回调已取消注册");
    }
}

实际应用场景

场景1:文件下载器

cs 复制代码
public class FileDownloader
{
    public async Task DownloadFileAsync(
        string url, 
        string savePath, 
        IProgress<int> progress = null,
        CancellationToken cancellationToken = default)
    {
        using var httpClient = new HttpClient();
        
        try
        {
            // 开始下载
            using var response = await httpClient.GetAsync(
                url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
                
            response.EnsureSuccessStatusCode();
            
            // 获取文件总大小
            var totalBytes = response.Content.Headers.ContentLength ?? -1;
            
            using var stream = await response.Content.ReadAsStreamAsync();
            using var fileStream = new FileStream(savePath, FileMode.Create);
            
            var buffer = new byte[8192];
            int bytesRead;
            long totalRead = 0;
            
            // 读取数据并写入文件
            while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
            {
                cancellationToken.ThrowIfCancellationRequested();
                
                await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
                totalRead += bytesRead;
                
                // 报告进度
                if (totalBytes > 0 && progress != null)
                {
                    int percentage = (int)((totalRead * 100) / totalBytes);
                    progress.Report(percentage);
                }
            }
            
            Console.WriteLine("下载完成!");
        }
        catch (OperationCanceledException)
        {
            // 如果文件已部分下载,删除不完整的文件
            if (File.Exists(savePath))
                File.Delete(savePath);
                
            Console.WriteLine("下载已被取消");
            throw;
        }
    }
}

// 使用示例
class Program
{
    static async Task Main()
    {
        var downloader = new FileDownloader();
        var cts = new CancellationTokenSource();
        
        // 创建进度报告器
        var progress = new Progress<int>(percent =>
        {
            Console.WriteLine($"下载进度: {percent}%");
        });
        
        // 启动下载任务
        var downloadTask = downloader.DownloadFileAsync(
            "https://example.com/largefile.zip",
            "largefile.zip",
            progress,
            cts.Token);
        
        // 模拟用户取消(5秒后)
        _ = Task.Delay(5000).ContinueWith(_ => 
        {
            Console.WriteLine("用户取消了下载");
            cts.Cancel();
        });
        
        try
        {
            await downloadTask;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("下载任务已成功取消");
        }
    }
}

场景2:数据库查询超时

cs 复制代码
public class DatabaseService
{
    public async Task<List<Product>> SearchProductsAsync(
        string keyword,
        TimeSpan timeout,
        CancellationToken externalToken = default)
    {
        // 创建查询超时
        using var timeoutCts = new CancellationTokenSource(timeout);
        
        // 组合令牌:外部取消 OR 超时取消
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            externalToken, timeoutCts.Token);
            
        try
        {
            // 模拟数据库查询
            await Task.Delay(2000, linkedCts.Token); // 假设查询需要2秒
            
            // 这里应该是真实的数据库查询代码
            // using var connection = new SqlConnection(connectionString);
            // await connection.OpenAsync(linkedCts.Token);
            // 执行查询...
            
            return new List<Product>
            {
                new Product { Id = 1, Name = $"{keyword} 产品1" },
                new Product { Id = 2, Name = $"{keyword} 产品2" }
            };
        }
        catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested)
        {
            throw new TimeoutException($"数据库查询超时,超过 {timeout.TotalSeconds} 秒");
        }
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

属性方法总结表

CancellationTokenSource 主要成员

成员 类型 说明
Token 属性 获取关联的 CancellationToken
IsCancellationRequested 属性 检查是否已请求取消
Cancel() 方法 请求取消操作
CancelAfter(TimeSpan) 方法 在指定时间后自动取消
CancelAfter(int) 方法 在指定毫秒数后自动取消
Dispose() 方法 释放资源
CreateLinkedTokenSource() 静态方法 创建链接的取消源

CancellationToken 主要成员

成员 类型 说明
IsCancellationRequested 属性 是否已请求取消
CanBeCanceled 属性 此令牌能否被取消
WaitHandle 属性 获取等待取消信号的句柄
None 静态属性 空令牌(无法取消)
ThrowIfCancellationRequested() 方法 如果已取消则抛出异常
Register(Action) 方法 注册取消回调
Equals() 方法 比较两个令牌
GetHashCode() 方法 获取哈希码

重要注意事项

  1. 资源释放:始终对 CancellationTokenSource 调用 Dispose

  2. 回调顺序:多个回调按注册顺序执行

  3. 线程安全:这些类型都是线程安全的

  4. 性能考虑:频繁检查 IsCancellationRequested 可能有性能影响

  5. 异常处理:OperationCanceledException 是预期的异常,应适当处理

相关推荐
介一安全3 小时前
【Frida Android】基础篇12:Native层hook基础——调用原生函数
android·网络安全·逆向·安全性测试·frida·1024程序员节
Halo_tjn3 小时前
Java Map集合
java·开发语言·计算机
lsx2024064 小时前
DOM 创建节点
开发语言
Cathyqiii4 小时前
Diffusion-TS:一种基于季节性-趋势分解与重构引导的可解释时间序列扩散模型
人工智能·神经网络·1024程序员节
存储国产化前线4 小时前
从浪涌防护到系统可控,天硕工业级SSD重构工业存储安全体系
ssd·固态硬盘·1024程序员节·工业级固态硬盘
瑞禧生物ruixibio4 小时前
4-ARM-PEG-Alkene(2)/Biotin(2),四臂聚乙二醇-烯烃/生物素多功能支链分子
1024程序员节
焦点链创研究所4 小时前
BUYCOIN:以社区共治重构加密交易版图,定义交易所3.0时代
1024程序员节
DO_Community4 小时前
DigitalOcean Gradient™ 平台上线 fal 四款多模态 AI 模型:快速生成图像与音频
1024程序员节
richxu202510014 小时前
C语言<<超全.超重要>>知识点总结
c语言·开发语言