这是一个bug。在底层,
Task.Run
调用
Task<Task>.Factory.StartNew
。内部的 Task 获取了正确的 Faulted 状态,但包装的 Task 没有获取到。
你可以通过调用以下方法来解决此问题:
Task.Factory.StartNew(() => { throw new OperationCanceledException("123", cts0.Token); }, cts1.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
尽管如此,您将失去 Task.Run 的另一个特性,即取消包装。请参阅:
Task.Run vs Task.Factory.StartNew
更多细节:
以下是 Task.Run 的代码,您可以看到它创建了一个包装的 UnwrapPromise(它派生自 Task<TResult>):
public static Task Run(Func<Task> function, CancellationToken cancellationToken)
{
if (function == null) throw new ArgumentNullException("function");
Contract.EndContractBlock();
cancellationToken.ThrowIfSourceDisposed();
if (cancellationToken.IsCancellationRequested)
return Task.FromCancellation(cancellationToken);
Task<Task> task1 = Task<Task>.Factory.StartNew(function, cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1, lookForOce: true);
return promise;
}
它调用的Task构造函数不接受取消标记(因此它不知道内部任务的取消标记)。请注意,它创建了一个默认的CancellationToken。这是它调用的构造函数:
internal Task(object state, TaskCreationOptions creationOptions, bool promiseStyle)
{
Contract.Assert(promiseStyle, "Promise CTOR: promiseStyle was false");
if ((creationOptions & ~TaskCreationOptions.AttachedToParent) != 0)
{
throw new ArgumentOutOfRangeException("creationOptions");
}
if ((creationOptions & TaskCreationOptions.AttachedToParent) != 0)
m_parent = Task.InternalCurrent;
TaskConstructorCore(null, state, default(CancellationToken), creationOptions, InternalTaskOptions.PromiseTask, null);
}
外层任务(UnwrapPromise
添加了一个后续操作)。后续操作检查内部任务。如果内部任务出现故障,它会将查找到的OperationCanceledException视为取消(无论是否匹配标记)。以下是 UnwrapPromise<TResult>.TrySetFromTask
(下面也显示了调用堆栈)。请注意其处于Faulted状态:
private bool TrySetFromTask(Task task, bool lookForOce)
else
break;
case TaskStatus.RanToCompletion:
var taskTResult = task as Task<TResult>;
result = TrySetResult(taskTResult != null ? taskTResult.Result : default(TResult));
break;
}
return result;
}
调用栈:
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetCanceled(System.Threading.CancellationToken tokenToRecord, object cancellationException) Line 645 C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.TrySetFromTask(System.Threading.Tasks.Task task, bool lookForOce) Line 6988 + 0x9f bytes C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.ProcessCompletedOuterTask(System.Threading.Tasks.Task task) Line 6956 + 0xe bytes C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.InvokeCore(System.Threading.Tasks.Task completingTask) Line 6910 + 0x7 bytes C#
mscorlib.dll!System.Threading.Tasks.UnwrapPromise<System.Threading.Tasks.VoidTaskResult>.Invoke(System.Threading.Tasks.Task completingTask) Line 6891 + 0x9 bytes C#
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations() Line 3571 C#
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Line 2323 + 0x7 bytes C#
mscorlib.dll!System.Threading.Tasks.Task.FinishStageTwo() Line 2294 + 0x7 bytes C#
mscorlib.dll!System.Threading.Tasks.Task.Finish(bool bUserDelegateExecuted) Line 2233 C#
mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 2785 + 0xc bytes C#
mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 2728 C#
mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 2664 + 0x7 bytes C#
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 829 C#
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 1170 + 0x5 bytes C#
它会注意OperationCanceledException,并调用TrySetCanceled将任务置于取消状态。
另一个需要注意的事情是,当您开始使用异步方法时,没有一种真正的方法可以向异步方法注册取消令牌。因此,在异步方法中遇到的任何OperationCancelledException都被认为是取消。参见
Associate a CancellationToken with an async method's Task。