【问题标题】:Task behaviour differs from Task<T> behaviour when OperationCanceledException thrown抛出 OperationCanceledException 时,任务行为与 Task<T> 行为不同
【发布时间】:2016-10-31 03:00:02
【问题描述】:

为什么这个测试失败了?
据我所知,t1t2 之间的唯一区别是t1Task,而t2Task&lt;int&gt;。然而由于某种原因,t2 最终处于 Faulted 状态,而不是 Canceled 状态。为什么行为会有所不同?

[Test]
public void Test_foo()
{
    var t1 = Task.Run(() =>
    {
        throw new OperationCanceledException();
    });
    try
    {
        t1.Wait();
    }
    catch (AggregateException e)
    {
        Assert.IsTrue(t1.IsCanceled);
    }

    var t2 = Task.Run(() =>
    {
        throw new OperationCanceledException();
        return 1;
    });
    try
    {
        t2.Wait();
    }
    catch (AggregateException e)
    {
        Assert.IsTrue(t2.IsCanceled); // fails, it's Faulted 
    }
}

【问题讨论】:

    标签: .net unit-testing task-parallel-library task


    【解决方案1】:

    您的任务之间的主要区别是您使用的 Task.Run 方法的重载:

    task1 是用Task.Run Method (Func&lt;Task&gt;) 创建的,而不是task2 是用Task.Run&lt;TResult&gt; Method (Func&lt;TResult&gt;) 创建的。这种重载确实创建了一些不同的任务:

    • 对于task1Result 属性设置为System.Threading.Tasks.VoidTaskResultCreationOptions 设置为None
    • 而不是task2CreationOptions 设置为DenyChildAttach,结果是default(int),即0

    当您等待task2 时,Result 属性未设置为实际值,因为引发了异常。根据MSDN

    当一个任务实例观察到一个OperationCanceledException 用户代码,它将异常的令牌与其关联的令牌进行比较 (传递给创建 Task 的 API 的那个)。如果他们 是相同的并且令牌的IsCancellationRequested 属性返回 true,任务将此解释为确认取消和 转换到Canceled 状态。如果您不使用WaitWaitAll 方法等待任务,然后任务只是将其状态设置为 Canceled.

    如果您正在等待转换到 Canceled 状态,System.Threading.Tasks.TaskCanceledException 抛出异常(包装在AggregateException 异常中)。笔记 该异常表示成功取消而不是 错误的情况。因此,任务的Exception 属性返回 null

    如果令牌的IsCancellationRequested 属性返回false如果异常的标记与任务的标记不匹配,则 OperationCanceledException 被视为普通异常,导致 转换到Faulted 状态的任务。另请注意, 其他异常的存在也会导致Task 转换为 Faulted 状态。您可以在 Status 财产。

    所以,在这里我们可以找到这种行为的原因 - 由于令牌不匹配,异常被视为正常异常。这很奇怪,因为令牌是definitely the same(我在Debug中检查过,哈希码相等,Equals方法和双等号运算符返回true),但比较仍然返回false。因此,您的情况的解决方案是显式使用 cancellation tokens,类似这样(我添加了 Thread.Sleep 以避免竞争条件):

    var t1TokenSource = new CancellationTokenSource();
    var t1 = Task.Run(() =>
    {
        Thread.Sleep(1000);
        if (t1TokenSource.Token.IsCancellationRequested)
        {
            t1TokenSource.Token.ThrowIfCancellationRequested();
        }
        //throw new TaskCanceledException();
    }, t1TokenSource.Token);
    try
    {
        t1TokenSource.Cancel();
        t1.Wait();
    }
    catch (AggregateException e)
    {
        Debug.Assert(t1.IsCanceled);
    }
    
    var t2TokenSource = new CancellationTokenSource();
    var t2 = Task.Run(() =>
    {
        Thread.Sleep(1000);
        if (t2TokenSource.Token.IsCancellationRequested)
        {
            t2TokenSource.Token.ThrowIfCancellationRequested();
        }
        //throw new TaskCanceledException();
        return 1;
    }, t2TokenSource.Token);
    try
    {
        t2TokenSource.Cancel();
        t2.Wait();
    }
    catch (AggregateException e)
    {
        Debug.Assert(t2.IsCanceled);
    }
    

    来自MSDN的另一句话:

    您可以使用以下选项之一终止操作:

    • 通过简单地从代理返回。在许多情况下,这已经足够了;但是,以这种方式取消的任务实例 转换到 TaskStatus.RanToCompletion 状态,而不是 TaskStatus.Canceled 州。
    • 通过抛出OperationCanceledException 并将请求取消的令牌传递给它。 最好的方法是 使用ThrowIfCancellationRequested 方法。 以这种方式取消转换到Canceled 状态,该状态 调用代码可以用来验证任务是否响应了它的 取消请求。

    如您所见,首选方式是可以预见的,而直接异常抛出则不行。另请注意,如果使用 task 也是使用 DenyChildAttach 创建的,并且没有 Result 属性,因此您遇到的构造函数存在一些差异。
    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-28
      相关资源
      最近更新 更多