C#.NET CancellationToken 深入与实战

简介

C# 异步编程中,"取消模式"是处理长时间运行任务中断的核心机制,主要通过 CancellationTokenSourceCancellationToken 协作实现。

基础取消模式

核心组件

  • CancellationTokenSource:创建和管理取消令牌。

  • CancellationToken:传递给任务,用于检查取消请求。

  • OperationCanceledException:取消时抛出的异常。

基本用法

csharp 复制代码
var cts = new CancellationTokenSource();
var token = cts.Token;

// 启动任务
var task = Task.Run(() => LongRunningOperation(token), token);

// 稍后取消
cts.Cancel();

任务内检查取消

csharp 复制代码
public void LongRunningOperation(CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        // 执行工作
        token.ThrowIfCancellationRequested(); // 检查并抛出异常
        Thread.Sleep(100);
    }
}

取消模式的变体

带超时的取消

csharp 复制代码
// 方法1:创建时设置超时
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

// 方法2:单独设置超时
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(5));

// 组合使用
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(5)); // 5分钟超时

// 处理超时异常
try
{
    await LongOperationAsync(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
    Console.WriteLine("操作超时");
}

组合多个取消令牌

csharp 复制代码
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

// 组合两个令牌,任一取消则触发
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    cts1.Token, 
    cts2.Token
);

await Task.Run(() => WorkAsync(linkedCts.Token), linkedCts.Token);

// 触发任一令牌都会取消任务
cts1.Cancel(); 

分阶段取消模式

csharp 复制代码
public async Task MultiStageProcessAsync(CancellationToken ct)
{
    // 阶段1:准备(不可取消)
    var data = LoadInitialData();
    
    // 阶段2:处理(可取消)
    using var stage2Cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
    await ProcessStageAsync(data, stage2Cts.Token);
    
    // 阶段3:保存(部分可取消)
    await SaveResultsAsync(data, ct); // 保存操作可取消
}

取消回调注册模式

csharp 复制代码
public async Task DownloadWithCleanupAsync(string url, CancellationToken ct)
{
    var tempFile = Path.GetTempFileName();
    
    try
    {
        // 注册取消时的清理操作
        using (ct.Register(() => 
        {
            if (File.Exists(tempFile))
                File.Delete(tempFile);
        }))
        {
            await DownloadToFileAsync(url, tempFile, ct);
            await ProcessFileAsync(tempFile, ct);
        }
    }
    finally
    {
        // 最终清理(无论是否取消)
        if (File.Exists(tempFile))
            File.Delete(tempFile);
    }
}

协作式取消模式

csharp 复制代码
public async Task CooperativeProcessingAsync(
    IProgress<int> progress, 
    CancellationToken ct)
{
    var items = GetItemsToProcess();
    int processed = 0;
    
    foreach (var item in items)
    {
        ct.ThrowIfCancellationRequested();
        
        await ProcessItemAsync(item, ct);
        processed++;
        
        progress?.Report(processed * 100 / items.Count);
    }
}

取消令牌传播模式

csharp 复制代码
// 服务层方法
public async Task<Report> GenerateReportAsync(
    ReportParameters parameters,
    CancellationToken ct)
{
    // 验证参数(快速操作,不检查取消)
    ValidateParameters(parameters);
    
    // 将取消令牌传播到所有子操作
    var data = await _dataService.FetchDataAsync(parameters, ct);
    var analysis = await _analysisService.AnalyzeAsync(data, ct);
    
    return await _reportService.CreateReportAsync(analysis, ct);
}

// 控制器调用
[HttpPost("reports")]
public async Task<IActionResult> GenerateReport(ReportRequest request)
{
    var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
    
    try
    {
        var report = await _reportService.GenerateReportAsync(
            request.ToParameters(), 
            cts.Token);
            
        return Ok(report);
    }
    catch (OperationCanceledException)
    {
        return StatusCode(499, "报告生成超时");
    }
}

取消异步流(IAsyncEnumerable)

csharp 复制代码
public async IAsyncEnumerable<int> GenerateItemsAsync(
    [EnumeratorCancellation] CancellationToken token = default)
{
    for (int i = 0; i < 100; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100, token);
        yield return i;
    }
}

// 消费时传递取消令牌
await foreach (var item in GenerateItemsAsync(token).WithCancellation(token))
{
    Console.WriteLine(item);
}

自定义扩展模式(如 WithCancellation)

  • 机制:扩展方法为不支持 CancellationTokenAPI 添加取消能力。

  • 适用场景:整合旧代码或第三方库到异步流程。

csharp 复制代码
public static async Task WithCancellation(this Task task, CancellationToken ct) {
    var tcs = new TaskCompletionSource();
    using (ct.Register(() => tcs.TrySetCanceled())) {
        if (task != await Task.WhenAny(task, tcs.Task)) {
            throw new OperationCanceledException(ct);
        }
        await task; // 确保任务异常正常传播
    }
}

// 使用示例
await SomeLegacyMethod().WithCancellation(ct);

Parallel.ForEachAsync 取消

  • 机制:.NET 6+ 的 Parallel.ForEachAsync 原生支持取消令牌。

  • 适用场景:并行处理集合且需统一取消所有子任务。

csharp 复制代码
await Parallel.ForEachAsync(data, new ParallelOptions { 
    CancellationToken = ct 
}, async (item, ct) => {
    await ProcessItemAsync(item, ct);
});

取消时终止所有并行任务,资源释放更高效

Task.Run 中的取消

csharp 复制代码
// 正确示例:CPU 密集型任务 + 取消
public async Task<int> CalculateAsync(CancellationToken ct) {
    return await Task.Run(() => {
        ct.ThrowIfCancellationRequested();
        return ComputeResult(); // 模拟计算
    }, ct);
}

与第三方 API 集成

csharp 复制代码
using var httpClient = new HttpClient();
var cts = new CancellationTokenSource(5000); // 5秒超时

try
{
    var response = await httpClient.GetAsync("https://example.com", cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("请求超时");
}

资源清理与取消

使用 CancellationToken.Register

注册回调在取消时执行清理:

csharp 复制代码
var cts = new CancellationTokenSource();
var token = cts.Token;

// 注册清理回调
using var registration = token.Register(() =>
{
    Console.WriteLine("执行资源清理...");
});

await Task.Run(() => WorkAsync(token), token);

在 using 语句中使用可取消资源

csharp 复制代码
public async Task WorkWithResourceAsync(CancellationToken token)
{
    using var resource = new MyDisposableResource();
    
    while (!token.IsCancellationRequested)
    {
        await resource.ProcessAsync(token);
    }
}

取消模式的最佳实践

传播取消令牌

  • 方法签名:始终将 CancellationToken 作为参数传递。

  • 默认值:使用 default 作为默认值,保持 API 灵活性。

csharp 复制代码
public async Task MyMethodAsync(CancellationToken token = default)
{
    await SomeOperationAsync(token); // 传递令牌
}

避免阻塞取消检查

使用 await Task.Delay(..., token) 而非 Thread.Sleep

csharp 复制代码
// 错误:阻塞线程,无法及时响应取消
while (!token.IsCancellationRequested)
{
    Thread.Sleep(1000); // 无法在睡眠期间响应取消
}

// 正确:可取消的延迟
while (!token.IsCancellationRequested)
{
    await Task.Delay(1000, token); // 支持取消
}

循环处理中的取消检查

csharp 复制代码
public async Task BatchProcessAsync(CancellationToken ct)
{
    var batch = GetNextBatch();
    
    while (batch != null && !ct.IsCancellationRequested)
    {
        await ProcessBatchAsync(batch, ct);
        
        // 在批处理间隙检查取消
        if (ct.IsCancellationRequested) break;
        
        batch = GetNextBatch();
    }
    
    ct.ThrowIfCancellationRequested();
}

优雅处理部分完成的操作

取消时可能需要回滚已完成的部分操作:

csharp 复制代码
public async Task ProcessTransactionAsync(CancellationToken token)
{
    try
    {
        await Step1Async(token);
        await Step2Async(token);
    }
    catch (OperationCanceledException)
    {
        await RollbackStep1Async(); // 回滚已完成的步骤
        throw;
    }
}

常见问题与解决方案

任务取消后仍在运行

  • 原因:任务内部未正确检查取消令牌。

  • 解决方案:确保在循环和关键操作中调用 ThrowIfCancellationRequested()

取消请求被忽略

  • 原因:使用了不支持取消的 API(如同步方法)。

  • 解决方案:使用异步版本的 API,或在 Task.Run 中执行同步操作并传递令牌。

资源泄漏

  • 原因:取消时未释放资源。

  • 解决方案:使用 using 语句、CancellationToken.Register 或实现IDisposable

高级技巧

实现可取消的重试逻辑

csharp 复制代码
public async Task RetryWithCancellationAsync(CancellationToken token)
{
    int retries = 3;
    while (retries > 0)
    {
        token.ThrowIfCancellationRequested();
        try
        {
            await OperationAsync(token);
            break;
        }
        catch (Exception) when (retries > 0)
        {
            retries--;
            await Task.Delay(1000, token); // 退避策略
        }
    }
}

取消令牌与并行处理

csharp 复制代码
public async Task ParallelProcessingAsync(CancellationToken ct)
{
    var items = GetItems();
    var tasks = new List<Task>();
    
    foreach (var item in items)
    {
        // 为每个任务创建链接令牌
        var itemCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        
        tasks.Add(ProcessItemAsync(item, itemCts.Token));
    }
    
    try
    {
        await Task.WhenAll(tasks);
    }
    catch (OperationCanceledException)
    {
        // 处理部分取消
        foreach (var task in tasks)
        {
            if (task.IsCanceled)
                LogCanceledItem(task);
        }
        
        throw;
    }
}

自定义取消条件

csharp 复制代码
public async Task ProcessUntilAsync(
    Func<bool> completionCondition,
    Func<bool> cancellationCondition,
    CancellationToken externalCt = default)
{
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);
    
    // 自定义取消条件
    var timer = new System.Timers.Timer(1000);
    timer.Elapsed += (s, e) => 
    {
        if (cancellationCondition())
            cts.Cancel();
    };
    timer.Start();
    
    try
    {
        while (!completionCondition() && !cts.Token.IsCancellationRequested)
        {
            await ProcessNextItemAsync(cts.Token);
        }
    }
    finally
    {
        timer.Stop();
        timer.Dispose();
    }
}

取消状态检查

csharp 复制代码
var task = operationWithTimeoutAsync(cts.Token);

try
{
    await task;
}
catch (OperationCanceledException)
{
    if (task.IsCanceled)
    {
        Console.WriteLine("任务被取消");
    }
    else if (task.IsFaulted)
    {
        Console.WriteLine($"任务失败: {task.Exception?.Message}");
    }
}

轻量级取消检查

csharp 复制代码
// 高频循环中的优化检查
public void ProcessItems(List<Item> items, CancellationToken ct)
{
    // 每100次迭代检查一次取消
    for (int i = 0; i < items.Count; i++)
    {
        // 优化:避免每次迭代都检查
        if (i % 100 == 0) ct.ThrowIfCancellationRequested();
        
        ProcessItem(items[i]);
    }
}

IsCancellationRequested VS ThrowIfCancellationRequested

核心区别

特性 IsCancellationRequested ThrowIfCancellationRequested
返回值 bool(是否请求取消) void(无返回值,直接抛异常)
异常处理 不会抛出异常 若请求取消,则抛出 OperationCanceledException
使用方式 需要手动检查并处理取消逻辑 自动触发取消逻辑,简化代码
适用场景 需要自定义取消处理逻辑(如部分回滚) 简单场景,快速终止任务

详细对比与示例

使用 IsCancellationRequested

  • 场景:需要在取消时执行自定义逻辑(如资源释放、部分结果保存)。
csharp 复制代码
public async Task CustomHandlingAsync(CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        // 执行部分工作
        await Step1Async();

        // 手动检查取消
        if (token.IsCancellationRequested)
        {
            await RollbackStep1Async(); // 自定义回滚逻辑
            token.ThrowIfCancellationRequested(); // 可选:抛出标准异常
        }

        await Step2Async();
    }
}

使用 ThrowIfCancellationRequested

  • 场景:快速响应取消请求,无需自定义中间逻辑。
csharp 复制代码
public async Task SimpleTaskAsync(CancellationToken token)
{
    for (int i = 0; i < 1000; i++)
    {
        token.ThrowIfCancellationRequested(); // 检查并抛异常
        await ProcessItemAsync(i);
    }
}

适用场景建议

优先使用 ThrowIfCancellationRequested 的场景

  • 简单循环任务:
csharp 复制代码
while (!token.IsCancellationRequested) // 繁琐
{
    // ...
}

// 简化为:
while (true)
{
    token.ThrowIfCancellationRequested(); // 简洁
    // ...
}
  • 调用第三方 API 时传递令牌:
csharp 复制代码
await HttpClient.GetAsync(url, token); // API内部会调用ThrowIfCancellationRequested
  • 快速失败的场景:
csharp 复制代码
public async Task ProcessFileAsync(string path, CancellationToken token)
{
    token.ThrowIfCancellationRequested(); // 提前检查
    await using var stream = File.OpenRead(path);
    // ...
}

必须使用 IsCancellationRequested 的场景

  • 需要分步处理取消:
csharp 复制代码
public async Task ComplexOperationAsync(CancellationToken token)
{
    var state = await LoadStateAsync();
    
    while (!token.IsCancellationRequested)
    {
        if (state.NeedsSave && token.IsCancellationRequested)
        {
            await SaveStateAsync(state); // 保存部分进度
            break;
        }
        
        // 继续处理
    }
}
  • 实现自定义取消逻辑:
csharp 复制代码
public async Task RetryWithCancellationAsync(CancellationToken token)
{
    int retries = 3;
    while (retries > 0 && !token.IsCancellationRequested)
    {
        try
        {
            await OperationAsync(token);
            break;
        }
        catch (Exception) when (retries-- > 0)
        {
            // 重试逻辑
        }
    }
}

组合使用示例

csharp 复制代码
public async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken token)
{
    // 在方法入口统一抛出
    token.ThrowIfCancellationRequested();

    foreach (var item in items)
    {
        // 在循环中,只检查一次
        if (token.IsCancellationRequested)
        {
            await CleanUpAsync();
            return;
        }

        // 真正的异步工作
        await ProcessItemAsync(item, token);

        // 也可以在此再次抛出
        token.ThrowIfCancellationRequested();
    }
}
  • 入口:用 ThrowIfCancellationRequested 保证一进来就能根据需要取消。

  • 循环中:先做 IsCancellationRequested 来决定是否清理再退出。

  • 关键点:在每个"可取消点"使用其中一种方法,保证及时响应。

总结

  • IsCancellationRequested

    • 只读检查,不 抛异常。

    • 柔性退出,适合先清理或报告再退出。

  • ThrowIfCancellationRequested()

    • 抛出 OperationCanceledException

    • 立即跳出,适合简洁地在任意位置统一响应取消。

结论指南

需求 推荐方法
循环退出条件 IsCancellationRequested
任务/方法边界检查 ThrowIfCancellationRequested
取消时需自定义清理 IsCancellationRequested
取消时应中断任务状态 ThrowIfCancellationRequested
高频调用点(>1000次/秒) IsCancellationRequested
关键操作检查点 ThrowIfCancellationRequested
相关推荐
向宇it43 分钟前
【unity实战】在Unity中实现不规则模型的网格建造系统(附项目源码)
游戏·3d·unity·c#·游戏引擎
CodeCraft Studio1 小时前
3D文档控件Aspose.3D实用教程:在 C# 中将 3MF 文件转换为 STL
c++·3d·c#
专注VB编程开发20年1 小时前
C#教程之NPOI读写excel文件XLS,XLSX格式
数据库·microsoft·c#·excel·xlsx·xls
东方.既白3 小时前
C# DataGridView 添加进度条
c#
界面开发小八哥10 小时前
DevExpress ASP.NET Web Forms v25.1新版本开发环境配置要求
c#·asp.net·界面控件·devexpress·ui开发
燃尽了,可无15 小时前
Socket通信与网络编程实战
c#·scket通信
钢铁男儿16 小时前
C# 异步编程(计时器)
开发语言·c#
weixin_447103581 天前
WPF之绑定!
c#·wpf
喵叔哟1 天前
41.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--网关集成Swagger
微服务·架构·.net