自取消Task子类导致其他实例执行异常问题求助
问题背景
我实现了Task的子类AutoCancelTask,目标是让它在释放时自动取消自身并等待完成。但单元测试时遇到异常:同一测试多次运行时,只有第一次能通过,后续全部失败,错误提示Assert.AreEqual failed. Expected:<True>. Actual:<(null)>。测试执行时长也异常:第一次约30ms,第二次约300ms,第四至五次<1ms>。
相关代码
AutoCancelTask 实现
using System; using System.Threading.Tasks; using System.Threading; namespace Tests { /// <summary> /// 释放时会自动取消并等待完成的Task /// </summary> public class AutoCancelTask : Task { private readonly CancellationTokenSource _cts; private AutoCancelTask(Action<CancellationToken> action, CancellationTokenSource cts, TaskCreationOptions options) : base(() => action(cts.Token), cts.Token, options) { _cts = cts; } /// <summary> /// 将可取消的Action包装为释放时自动取消的Task /// </summary> /// <param name="action">要包装的Action</param> /// <param name="options">Task创建选项</param> /// <returns>已启动的Task</returns> public static Task Run(Action<CancellationToken> action, TaskCreationOptions options = TaskCreationOptions.None) { Task t = new AutoCancelTask(action, new CancellationTokenSource(), options); t.Start(); return t; } protected override void Dispose(bool disposing) { if (disposing) { try { _cts.Cancel(); } catch (ObjectDisposedException) { // 已释放时无需处理 } try { Wait(); } catch (ObjectDisposedException) { } catch (AggregateException ex) { if (1 < ex.InnerExceptions.Count || !(ex.InnerExceptions[0] is TaskCanceledException)) { // 仅当聚合异常不是单个TaskCanceledException时才重新抛出 throw ex; } } _cts.Dispose(); } base.Dispose(disposing); } } }
测试代码
using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Threading; namespace Tests { public class TestDataWrapper { internal bool? IsCancelled { get; private set; } = null; internal void Action(CancellationToken cancel) { IsCancelled = Task.Delay(100, cancel).ContinueWith(t => t.IsCanceled).Result; } } [TestClass()] public class Tests { public static IEnumerable<object[]> Datas { get; } = Enumerable.Range(0, 5).Select(_ => new object[] { new TestDataWrapper() }); [DataTestMethod] [DynamicData(nameof(Datas))] public void AutoCancelTask_Cancels(TestDataWrapper data) { using (AutoCancelTask.Run(data.Action)) { } Assert.AreEqual(true, data.IsCancelled); } } }
问题根源分析
核心问题出在**Task子类的构造逻辑与Start()调用的时序竞争**,以及Task基类的内部行为特性:
任务调度的异步性:调用
AutoCancelTask.Run时,先创建AutoCancelTask实例,再调用t.Start()。但Task的基类构造函数中传入的委托() => action(cts.Token)不会立即执行,要等到Start()将任务调度到线程池后才会运行。而using语句会在任务创建后立即触发Dispose()流程,此时_cts.Cancel()可能在任务委托被调度执行前就已执行。Task对已取消令牌的处理逻辑:如果任务启动时(Start()执行后),绑定的CancellationToken已经处于取消状态,Task基类会直接将任务标记为Canceled状态,完全跳过委托的执行。这就导致后续测试中,Action方法根本没运行,IsCancelled始终为null。第一次测试通过的原因:首次运行时线程池空闲,任务被立即调度执行,
Action已经开始运行,此时Cancel()触发后,Task.Delay会响应取消并将IsCancelled设为true。而后续测试中,线程池调度速度跟不上Dispose()的执行速度,令牌先被取消,任务直接跳过委托执行。
添加1ms延迟能临时解决问题,本质是给了任务足够的调度执行时间,让委托能在令牌取消前开始运行。
修复方案
Task类并非设计用于继承,官方文档明确建议优先使用组合而非继承实现自定义任务逻辑。正确的实现方式是创建一个实现IDisposable的类,内部持有任务和取消令牌源:
public class AutoCancelTask : IDisposable { private readonly Task _task; private readonly CancellationTokenSource _cts; private AutoCancelTask(Action<CancellationToken> action, CancellationTokenSource cts, TaskCreationOptions options) { _cts = cts; // 直接用Task.Factory.StartNew调度任务,确保委托会被执行(即使令牌后续被取消) _task = Task.Factory.StartNew(() => action(cts.Token), cts.Token, options, TaskScheduler.Default); } public static AutoCancelTask Run(Action<CancellationToken> action, TaskCreationOptions options = TaskCreationOptions.None) { return new AutoCancelTask(action, new CancellationTokenSource(), options); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { try { _cts.Cancel(); } catch (ObjectDisposedException) { // 已释放时无需处理 } try { _task.Wait(); } catch (ObjectDisposedException) { // 已释放时无需处理 } catch (AggregateException ex) { if (!(ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)) { throw; } } _cts.Dispose(); } } }
对应的测试代码需要调整using语句的使用方式:
[DataTestMethod] [DynamicData(nameof(Datas))] public void AutoCancelTask_Cancels(TestDataWrapper data) { using (var autoCancelTask = AutoCancelTask.Run(data.Action)) { } Assert.AreEqual(true, data.IsCancelled); }
这种实现方式避免了Task基类的内部状态竞争问题,即使令牌被快速取消,任务委托也会被执行(直到逻辑内部检查令牌或调用依赖令牌的API),确保IsCancelled会被正确赋值。
内容的提问来源于stack exchange,提问作者GreatBarrier

