一、当任务拒绝停止时:真实场景的困扰

某次深夜值班时,我们的订单处理服务突然出现CPU爆满告警。定位发现某个批量导出的后台任务在用户取消操作后仍在疯狂消耗资源。这种"僵尸任务"的典型表现正是没有正确实现任务取消机制。在ASP.NET Core中,我们常用Task处理异步操作,但很多开发者会遇到这样的窘境:

  • 用户取消请求后接口仍在执行
  • 后台任务无法响应应用关闭信号
  • 长时间任务导致内存泄漏
  • 异步操作超时设置形同虚设

(示例场景:电商平台订单导出功能)

// 错误示例:没有取消机制的导出任务
public async Task<IActionResult> ExportOrders()
{
    var data = await GenerateReportAsync(); // 耗时操作
    return File(data, "application/zip");
}

// 正确做法应该加入取消支持

二、CancellationToken的完整使用指南

2.1 基础配置:从Controller到Task的传递

ASP.NET Core会自动为每个请求创建CancellationToken,通过Controller基类的HttpContext.RequestAborted获取:

public class ReportController : Controller
{
    [HttpGet]
    public async Task<IActionResult> ExportLargeReport()
    {
        try
        {
            // 获取与请求关联的取消令牌
            var cancellationToken = HttpContext.RequestAborted;
            
            var reportData = await GenerateReportAsync(cancellationToken);
            return File(reportData, "text/csv");
        }
        catch (OperationCanceledException)
        {
            return StatusCode(499); // 客户端主动断开连接
        }
    }

    private async Task<byte[]> GenerateReportAsync(CancellationToken ct)
    {
        var data = new List<byte>();
        for (int i = 0; i < 100; i++)
        {
            ct.ThrowIfCancellationRequested(); // 关键检查点
            data.AddRange(await QueryDatabaseChunkAsync(i));
            await Task.Delay(100); // 模拟处理耗时
        }
        return data.ToArray();
    }
}

2.2 高级场景:组合取消令牌

当需要同时响应请求取消和自定义超时:

public async Task ProcessDataAsync(CancellationToken requestToken)
{
    using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
    using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
        requestToken, 
        timeoutCts.Token);

    await RunDataPipelineAsync(combinedCts.Token);
}

2.3 异步方法的正确取消姿势

处理数据库操作的正确示例(使用Dapper):

public async Task<List<Order>> QueryOrdersAsync(CancellationToken ct)
{
    using var connection = new SqlConnection(_config.GetConnectionString("Default"));
    await connection.OpenAsync(ct); // 支持取消的OpenAsync
    
    return (await connection.QueryAsync<Order>(
        "SELECT * FROM Orders WHERE Status = @status",
        new { status = "pending" },
        cancellationToken: ct)) // 显式传递取消令牌
        .ToList();
}

三、典型问题排查手册

3.1 忽略令牌传递的灾难现场

错误示例分析:

// 错误示例:令牌未传递到底层方法
public async Task ProcessAsync(CancellationToken ct)
{
    await Step1(); // 没有传递ct
    await Step2(); // 令牌在此中断
}

private async Task Step1()
{
    await Task.Delay(1000); // 无法取消
}

3.2 令牌穿透的正确姿势

正确解决方案:

public async Task ProcessAsync(CancellationToken ct)
{
    await Step1(ct);
    await Step2(ct); // 逐层传递令牌
}

private async Task Step1(CancellationToken ct)
{
    await Task.Delay(1000, ct); // 支持取消的Delay
}

四、实战中的进阶技巧

4.1 后台服务的优雅关闭

在IHostedService中实现:

public class BackgroundWorker : IHostedService
{
    private CancellationTokenSource _cts;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        
        _ = Task.Run(async () =>
        {
            while (!_cts.IsCancellationRequested)
            {
                await DoWork(_cts.Token);
                await Task.Delay(5000, _cts.Token);
            }
        });
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _cts?.Cancel();
        return Task.CompletedTask;
    }
}

4.2 并行任务的取消控制

使用WhenAny处理多个并行任务:

public async Task<string> FetchFirstResultAsync(CancellationToken ct)
{
    var client = new HttpClient();
    var urls = new[] { "api1", "api2", "api3" };

    var tasks = urls.Select(url => 
        client.GetStringAsync(url, ct));

    var completedTask = await Task.WhenAny(tasks);
    ct.ThrowIfCancellationRequested(); // 检查是否因为取消而完成
    return await completedTask;
}

五、关键知识点总结

5.1 应用场景分析

  • Web请求取消响应(用户关闭浏览器)
  • 后台任务强制终止(应用关闭时)
  • 操作超时控制(防止长时间阻塞)
  • 资源释放保障(数据库连接等)

5.2 技术方案对比

方法 优点 缺点
CancellationToken 精准控制,资源开销小 需要逐层传递
Task超时设置 使用简单 无法响应外部取消事件
强制终止Thread 立即生效 可能引发状态不一致
第三方Cancellation库 提供高级功能 增加依赖

5.3 必须遵守的军规

  1. 异步方法必须接受CancellationToken参数
  2. 每个耗时操作前进行IsCancellationRequested检查
  3. 使用支持取消的API版本(如Task.Delay(TimeSpan, CancellationToken))
  4. 正确处理ObjectDisposedException
  5. 避免在finally块中使用取消令牌

5.4 最佳实践路线

graph TD
    A[接收取消请求] --> B[创建CancellationTokenSource]
    B --> C[传递到所有异步方法]
    C --> D[在关键节点检查IsCancellationRequested]
    D --> E[正确抛出OperationCanceledException]
    E --> F[上层捕获处理取消逻辑]