c# - Task.Wait 在 OperationCanceledException 情况下的意外行为

标签 c# .net task-parallel-library wait cancellation

考虑以下代码:

CancellationTokenSource cts0 = new CancellationTokenSource(), cts1 = new CancellationTokenSource();
try
{
    var task = Task.Run(() => { throw new OperationCanceledException("123", cts0.Token); }, cts1.Token);
    task.Wait();
}
catch (AggregateException ae) { Console.WriteLine(ae.InnerException); }

由于MSDN任务应处于 Faulted 状态,因为它的 token 与异常的 token 不匹配(并且 IsCancellationRequestedfalse):

If the token's IsCancellationRequested property returns false or if the exception's token does not match the Task's token, the OperationCanceledException is treated like a normal exception, causing the Task to transition to the Faulted state.

当我使用 .NET 4.5.2 在控制台应用程序中启动此代码时,我得到处于 Canceled 状态的任务(聚合异常包含未知的 TaskCanceledExeption,而不是原始的)。并且原始异常的所有信息都丢失了(消息、内部异常、自定义数据)。

我还注意到,在 OperationCanceledException 情况下,Task.Wait 的行为不同于 await task

try { Task.Run(() => { throw new InvalidOperationException("123"); }).Wait(); } // 1
catch (AggregateException ae) { Console.WriteLine(ae.InnerException); }

try { await Task.Run(() => { throw new InvalidOperationException("123"); }); } // 2
catch (InvalidOperationException ex) { Console.WriteLine(ex); }

try { Task.Run(() => { throw new OperationCanceledException("123"); }).Wait(); } // 3 
catch (AggregateException ae) { Console.WriteLine(ae.InnerException); }

try { await Task.Run(() => { throw new OperationCanceledException("123"); }); } // 4
catch (OperationCanceledException ex) { Console.WriteLine(ex); }

案例 12 产生几乎相同的结果(仅在 StackTrace 中不同),但是当我将异常更改为 OperationCanceledException,然后我得到非常不同的结果:一个未知的 TaskCanceledException 如果 3 没有原始数据,预期的 OpeartionCanceledException 如果 4 包含所有原始数据(消息等)。

所以问题是:MSDN 是否包含不正确的信息?还是 .NET 中的错误?或者也许只是我不明白什么?

最佳答案

这是一个错误。 Task.Run引擎盖下调用 Task<Task>.Factory.StartNew .此内部任务正在获得正确的故障状态。包装任务不是。

您可以通过调用来解决此错误

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)
{
    // Check arguments
    if (function == null) throw new ArgumentNullException("function");
    Contract.EndContractBlock();

    cancellationToken.ThrowIfSourceDisposed();

    // Short-circuit if we are given a pre-canceled token
    if (cancellationToken.IsCancellationRequested)
        return Task.FromCancellation(cancellationToken);

    // Kick off initial Task, which will call the user-supplied function and yield a Task.
    Task<Task> task1 = Task<Task>.Factory.StartNew(function, cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

    // Create a promise-style Task to be used as a proxy for the operation
    // Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown as faults from task1, to support in-delegate cancellation.
    UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1, lookForOce: true);

    return promise;
}

它调用的任务构造函数不带取消标记(因此它不知道内部任务的取消标记)。请注意,它会创建一个默认的 CancellationToken。这是它调用的 ctor:

internal Task(object state, TaskCreationOptions creationOptions, bool promiseStyle)
{
    Contract.Assert(promiseStyle, "Promise CTOR: promiseStyle was false");

    // Check the creationOptions. We only allow the AttachedToParent option to be specified for promise tasks.
    if ((creationOptions & ~TaskCreationOptions.AttachedToParent) != 0)
    {
        throw new ArgumentOutOfRangeException("creationOptions");
    }

    // m_parent is readonly, and so must be set in the constructor.
    // Only set a parent if AttachedToParent is specified.
    if ((creationOptions & TaskCreationOptions.AttachedToParent) != 0)
        m_parent = Task.InternalCurrent;

    TaskConstructorCore(null, state, default(CancellationToken), creationOptions, InternalTaskOptions.PromiseTask, null);
}

外部任务(UnwrapPromise 添加了一个延续)。继续检查内部任务。在内部任务出错的情况下,它认为找到一个 OperationCanceledException 作为指示取消(不管匹配的 token )。下面是 UnwrapPromise<TResult>.TrySetFromTask (下面也是显示调用位置的调用堆栈)。注意故障状态:

private bool TrySetFromTask(Task task, bool lookForOce)
{
    Contract.Requires(task != null && task.IsCompleted, "TrySetFromTask: Expected task to have completed.");

    bool result = false;
    switch (task.Status)
    {
        case TaskStatus.Canceled:
            result = TrySetCanceled(task.CancellationToken, task.GetCancellationExceptionDispatchInfo());
            break;

        case TaskStatus.Faulted:
            var edis = task.GetExceptionDispatchInfos();
            ExceptionDispatchInfo oceEdi;
            OperationCanceledException oce;
            if (lookForOce && edis.Count > 0 &&
                (oceEdi = edis[0]) != null &&
                (oce = oceEdi.SourceException as OperationCanceledException) != null)
            {
                result = TrySetCanceled(oce.CancellationToken, oceEdi);
            }
            else
            {
                result = TrySetException(edis);
            }
            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 将任务置于已取消状态。

旁白:

还有一点要注意,当你开始使用async方法,实际上没有办法用 async 注册取消 token 方法。因此,在异步方法中遇到的任何 OperationCancelledException 都被视为取消。 看 Associate a CancellationToken with an async method's Task

关于c# - Task.Wait 在 OperationCanceledException 情况下的意外行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25488357/

相关文章:

c# - 在 C# 中显示后立即关闭窗体

c# - DispatcherTimer - 如果前一个滴答仍在运行,则防止触发滴答事件

c# - asp.net mvc highchart 折线图 json

c# - 我应该在 Stream.CopyTo 之前使用 Stream.Flush 吗?

c# - 推迟任务的启动<T>

c# - 为什么 TaskFactory.FromAsync() 重载需要提供状态对象?

c# - TPL Parallel.ForEach 中的每线程实例对象

C#:检查类型 T 是否为 bool

c# - Microsoft Edge 未使用激活的 Windows 身份验证加载 CSS 文件

c# - 如何将小工具集成到我的 .NET 应用程序中