CancellationToken未执行取消操作 异步任务持续运行问题排查
核心错误点
你的代码一共有3处关键问题,直接导致取消逻辑完全失效:
- 令牌取值错误:你写的
var token = _tokenSource.IsCancellationRequested;拿到的是布尔类型的状态值,不是可传递给异步方法的CancellationToken令牌。IsCancellationRequested仅用来判断当前是否已触发取消,真正要传给Task.Delay等异步方法的令牌对象是_tokenSource.Token属性,你传一个固定的false布尔值,异步方法根本接收不到任何取消信号。 - 生命周期认知错误:ASP.NET MVC/Core的Controller是按请求瞬态创建的,每一个HTTP请求进入服务端,都会生成一个全新的Controller实例。你把
_tokenSource存在Controller的私有实例字段里,Trace请求持有的_tokenSource和CancelRequest请求持有的_tokenSource完全是两个互不相关的对象,Cancel接口调用Cancel()方法根本碰不到Trace请求里的令牌源,自然无法触发取消,甚至大概率会因为_tokenSource为null抛出空引用异常。 - 资源释放逻辑风险:你在finally块中直接释放
_tokenSource,如果取消请求在Trace方法执行到资源释放逻辑之后到达,会直接触发对象已释放异常,完全无法命中取消逻辑。
修正方案
不要把CancellationTokenSource存在Controller私有字段中,需要用全局可访问的存储结构,为每个追踪任务生成唯一ID,按ID关联存储对应的令牌源,避免不同用户、不同任务之间的令牌源互相覆盖。
以ASP.NET Core为例,可直接使用内置的IMemoryCache存储令牌源,老版ASP.NET MVC可以用线程安全的ConcurrentDictionary做存储,额外加定时清理逻辑避免内存泄漏即可。
服务端代码修正
- 先在Program.cs中注册内存缓存服务:
builder.Services.AddMemoryCache();
- 修正Controller逻辑:
public class TraceController : Controller { private readonly IMemoryCache _cache; // 构造函数注入缓存实例 public TraceController(IMemoryCache cache) { _cache = cache; } [HttpPost] public async Task<ActionResult> Trace(TraceModel model) { // 为当前任务生成唯一标识 var taskId = Guid.NewGuid().ToString("N"); // 将任务ID传递到前端,取消时需要带回这个ID ViewBag.CurrentTaskId = taskId; var cts = new CancellationTokenSource(); // 令牌源存入缓存,设置1分钟绝对过期避免内存泄漏 _cache.Set($"TraceTaskCts_{taskId}", cts, TimeSpan.FromMinutes(1)); try { // 修正:传入正确的CancellationToken对象 await Task.Delay(15000, cts.Token); cts.Token.ThrowIfCancellationRequested(); // 此处编写你的实际业务逻辑 } catch (OperationCanceledException) { // 主动触发的取消属于正常流程,不需要按错误日志记录 Logger.Info("Trace task canceled by user"); } catch (Exception ex) { Logger.Error(ex.GetBaseException().Message); } finally { // 任务结束后先移除缓存中的令牌源,再释放资源 _cache.Remove($"TraceTaskCts_{taskId}"); cts.Dispose(); } return View(model); } [HttpPost] public ActionResult CancelRequest(string taskId) { // 根据前端传回的任务ID找到对应的令牌源,触发取消 if (_cache.TryGetValue<CancellationTokenSource>($"TraceTaskCts_{taskId}", out var cts)) { cts.Cancel(); } return new EmptyResult(); } }
- 视图页添加隐藏域存储任务ID:
<input type="hidden" id="currentTaskId" value="@ViewBag.CurrentTaskId" />
前端JS代码修正
取消请求需要携带当前运行任务的ID,确保能匹配到正确的令牌源:
function CancelRequest() { const currentTaskId = document.getElementById("currentTaskId").value; if (!currentTaskId) { alert("当前没有运行中的任务"); return; } $.ajax({ type: 'POST', url: '@Url.Action("CancelRequest", "Trace")', data: { taskId: currentTaskId }, success: function (results) { console.log(results); alert("请求已取消"); } }); }
注意:绝对不要用静态字段存单个CancellationTokenSource实例,否则多用户同时发起追踪请求时,后启动的任务会覆盖之前的令牌源,点击取消会误取消其他用户的任务。
内容的提问来源于stack exchange,提问作者Tumo Maraisane
相关产品推荐
相关产品推荐

