在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
相关产品推荐
相关产品推荐

