ASP.NET Core中如何通过DELETE端点取消长时间运行的任务?
长时任务API的取消方案实现
现有API与问题背景
你已经实现了长时任务的启动、进度查询接口,核心问题是如何实现可靠的任务取消功能——既不想让服务层持有令牌源,也不能用静态令牌源限制并发任务,而Action自带的CancellationToken仅适用于当前请求生命周期,无法满足跨请求取消的需求。
你的现有代码如下:
[HttpGet] public IActionResult LongRunning() { var taskId = longRunningService.CreateTask(); var progress = new ProgressDto { Id = taskId, Status = "created" }; return CreatedAtAction("LongRunningProgress", new { id = taskId }, progress); } [HttpGet("{id:int}")] public async Task<ActionResult<ProgressDto>> LongRunningProgress(int id) { var task = longRunningService.GetTask(id); // 根据任务完成状态或部分结果返回 } [HttpDelete("{id:int}")] public ActionResult CancelLongRunning(int id) { // 如何实现? }
可行解决方案
方案1:服务层维护任务与CancellationTokenSource的映射
这是单实例场景下最直接的方案,核心是在服务层用线程安全的字典,将每个任务ID与对应的CancellationTokenSource绑定,同时做好资源清理。
服务层核心实现
public class LongRunningService { // 线程安全字典:任务ID -> 取消令牌源 private readonly ConcurrentDictionary<int, CancellationTokenSource> _taskCtsMap = new(); // 存储任务状态、进度等信息 private readonly ConcurrentDictionary<int, TaskInfo> _taskInfos = new(); private int _nextTaskId = 0; public int CreateTask() { var taskId = Interlocked.Increment(ref _nextTaskId); var cts = new CancellationTokenSource(); _taskCtsMap.TryAdd(taskId, cts); _taskInfos.TryAdd(taskId, new TaskInfo { Status = "created" }); // 启动后台长时任务,传入取消令牌 _ = RunLongTask(taskId, cts.Token); return taskId; } public TaskInfo GetTask(int id) { _taskInfos.TryGetValue(id, out var info); return info; } public bool CancelTask(int id) { // 移除令牌源并取消任务 if (_taskCtsMap.TryRemove(id, out var cts)) { cts.Cancel(); cts.Dispose(); // 及时释放资源 // 更新任务状态 if (_taskInfos.TryGetValue(id, out var info)) { info.Status = "cancelled"; } return true; } return false; // 任务不存在或已完成 } private async Task RunLongTask(int taskId, CancellationToken cancellationToken) { var taskInfo = _taskInfos[taskId]; taskInfo.Status = "running"; try { // 模拟长时任务逻辑,定期检查取消信号 for (int i = 0; i < 10; i++) { // 检查取消请求 if (cancellationToken.IsCancellationRequested) { taskInfo.Status = "cancelled"; return; } // 执行子任务,传入取消令牌 await Task.Delay(1000, cancellationToken); taskInfo.Progress = (i + 1) * 10; } taskInfo.Status = "completed"; taskInfo.Result = "任务最终结果"; } catch (OperationCanceledException) { taskInfo.Status = "cancelled"; } finally { // 清理剩余的令牌源 _taskCtsMap.TryRemove(taskId, out var cts); cts?.Dispose(); } } } // 任务信息辅助类 public class TaskInfo { public string Status { get; set; } public int Progress { get; set; } public string Result { get; set; } }
控制器取消接口实现
[HttpDelete("{id:int}")] public ActionResult CancelLongRunning(int id) { var isCancelled = longRunningService.CancelTask(id); return isCancelled ? NoContent() : NotFound(); }
关键注意点
- 用
ConcurrentDictionary保证多请求下的线程安全 - 任务完成或取消后必须清理
CancellationTokenSource,避免内存泄漏 - 长时任务内部要主动检查取消令牌,或在调用支持取消的异步方法时传入令牌
方案2:分布式取消(集群部署场景)
如果API是多实例集群部署,单实例内存字典无法跨实例生效,此时可以用分布式缓存(如Redis)存储取消信号:
- 创建任务时,在Redis中写入
task:{id}:cancel键,值为false - 取消任务时,将该键值更新为
true - 长时任务定期轮询Redis中的该键,若值为
true则终止任务 - 任务完成后删除该键
这种方案需要引入分布式缓存依赖,但能适配集群环境。
误区澄清
- 禁止使用控制器静态
CancellationTokenSource:会导致所有任务共享同一令牌,取消一个任务会触发所有任务取消,完全不符合并发需求 - Action自带的
CancellationToken仅对应当前请求的生命周期,仅在客户端断开连接时触发,无法实现主动的跨请求取消,不适用当前场景
内容的提问来源于stack exchange,提问作者FlamingDrake
相关产品推荐
相关产品推荐

