You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在ASP.NET Core 3.1中用取消令牌终止Task.WhenAll多任务执行

ASP.NET Core 多下载任务取消功能修复方案

核心问题

当前实现存在以下关键问题导致无法取消下载:

  • 控制器中cts为类级变量,多用户场景会互相干扰,且无触发取消的接口
  • 下载循环中未主动检查取消令牌,已启动的循环不会响应取消信号
  • 异常捕获逻辑吞掉了TaskCanceledException,导致取消信号无法向上传递
  • BackgroundService的使用方式不符合需求,下载任务脱离请求上下文,关闭页面仍会后台运行

具体修复步骤

1. 重构控制器,隔离用户会话并添加取消接口

将类级cts改为会话级存储,避免多用户冲突,同时新增取消触发接口:

public class DownloadsController : Controller
{
    private readonly IManageFileDownlods _downloadManager;
    // 用会话ID作为key,存储每个用户的取消令牌源
    private static readonly Dictionary<string, CancellationTokenSource> _userCts = new();

    public DownloadsController(IManageFileDownlods downloadManager)
    {
        _downloadManager = downloadManager;
    }

    [HttpPost]
    public async Task<IActionResult> DownloadFiles(DownloadFilesModel downloadFilesModel)
    {
        var sessionId = HttpContext.Session.Id;
        var cts = new CancellationTokenSource();
        _userCts[sessionId] = cts;

        try
        {
            var successFiles = await _downloadManager.DownloadAsync(downloadFilesModel.param1, downloadFilesModel.param2, cts.Token);
            // 下载完成后清理令牌
            _userCts.Remove(sessionId);
            return Ok(successFiles);
        }
        catch (TaskCanceledException)
        {
            _userCts.Remove(sessionId);
            return StatusCode(StatusCodes.Status499ClientClosedRequest, "下载已取消");
        }
    }

    [HttpPost]
    public IActionResult CancelDownload()
    {
        var sessionId = HttpContext.Session.Id;
        if (_userCts.TryGetValue(sessionId, out var cts))
        {
            cts.Cancel();
            _userCts.Remove(sessionId);
        }
        return Ok("已触发取消");
    }
}

2. 修复ManageFileDownlods的取消响应逻辑

移除异常吞掉逻辑,在每次循环迭代前检查取消令牌,同时调整并发控制逻辑:

public class ManageFileDownlods : IManageFileDownlods // 移除BackgroundService继承,让任务随请求上下文绑定
{
    private readonly IDownloadmaster _downloadmaster;

    public ManageFileDownlods(IDownloadmaster downloadmaster)
    {
        _downloadmaster = downloadmaster;
    }

    public async Task<List<DownloadFile>> DownloadAsync(object funcparam1, object funcparam2, CancellationToken cancellationToken = default)
    {
        var successFiles = new List<DownloadFile>();
        var downloadTasksList = new List<Task<List<DownloadFile>>>();

        while (serverQueue.Count > 0)
        {
            // 每次循环先检查取消令牌
            cancellationToken.ThrowIfCancellationRequested();

            downloadTasksList.Add(ServerProcess(funcparam1, funcparam2, cancellationToken));
            // 控制并发数,避免一次性创建过多任务
            if (downloadTasksList.Count >= 5)
            {
                var completedTask = await Task.WhenAny(downloadTasksList);
                successFiles.AddRange(await completedTask);
                downloadTasksList.Remove(completedTask);
            }
        }

        // 等待剩余任务完成
        if (downloadTasksList.Count > 0)
        {
            var remainingResults = await Task.WhenAll(downloadTasksList);
            successFiles.AddRange(remainingResults.SelectMany(r => r));
        }

        return successFiles;
    }

    private async Task<List<DownloadFile>> ServerProcess(object funcparam1, object funcparam2, CancellationToken cancellationToken)
    {
        var serverFiles = new List<DownloadFile>();
        var fileDownlodTasks = new List<Task<DownloadFile>>();
        var paramQueue = funcparam1 as Queue<object> ?? throw new InvalidOperationException("参数需为队列类型");

        while (paramQueue.Count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();

            // 控制同时下载5个文件
            for (int i = 0; i < 5 && paramQueue.Count > 0; i++)
            {
                var param = paramQueue.Dequeue();
                fileDownlodTasks.Add(_downloadmaster.Download(param, funcparam2, cancellationToken));
                await Task.Delay(300, cancellationToken); // 传递取消令牌给延迟操作
            }

            try
            {
                var completedFiles = await Task.WhenAll(fileDownlodTasks);
                serverFiles.AddRange(completedFiles);
                fileDownlodTasks.Clear();
            }
            catch (TaskCanceledException)
            {
                // 直接抛出异常,让上层感知取消
                throw;
            }
        }

        return serverFiles;
    }
}

3. 确保底层下载逻辑支持取消

修改Downloadmaster的下载方法,确保底层请求响应取消令牌:

public async Task<DownloadFile> Download(object param1, object param2, CancellationToken cancellationToken)
{
    // 先检查取消状态
    cancellationToken.ThrowIfCancellationRequested();

    // 底层下载方法必须传入取消令牌
    var fileContents = await DownloadFileFromServer(param1, param2, cancellationToken);

    return new DownloadFile
    {
        // 填充文件属性
        FileContent = fileContents,
        FileName = param1.ToString()
    };
}

// 示例底层下载实现(需支持取消)
private async Task<byte[]> DownloadFileFromServer(object param1, object param2, CancellationToken cancellationToken)
{
    using var httpClient = new HttpClient();
    var fileUrl = param1.ToString();
    
    // 传入取消令牌中断网络请求
    using var response = await httpClient.GetAsync(fileUrl, cancellationToken);
    response.EnsureSuccessStatusCode();
    
    // 读取内容时也传递取消令牌
    return await response.Content.ReadAsByteArrayAsync(cancellationToken);
}

4. Razor页面取消按钮实现

在页面中添加取消按钮,发送POST请求触发取消:

<button id="cancelDownloadBtn" class="btn btn-danger">取消下载</button>

<script>
document.getElementById('cancelDownloadBtn').addEventListener('click', async () => {
    const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
    const response = await fetch('/Downloads/CancelDownload', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'RequestVerificationToken': token
        }
    });
    const result = await response.text();
    alert(result);
});
</script>

关键注意事项

  • 启用Session支持:在Program.cs中添加builder.Services.AddSession();和app.UseSession();,确保会话ID有效。
  • 避免后台任务脱离请求:移除ManageFileDownlods对BackgroundService的继承,让下载任务与用户请求绑定,关闭页面后任务会随请求上下文终止。
  • 并发数控制:通过Task.WhenAny控制同时运行的任务数量,防止服务器资源耗尽。

内容的提问来源于stack exchange,提问作者Pwavel002

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 09:10:27