【问题标题】:Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequestedCancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态
【发布时间】:2014-06-23 06:37:01
【问题描述】:

通常我不会发布带有答案的问题,但这次我想引起人们对我认为可能是一个晦涩但普遍的问题的关注。它是由this question 触发的,从那以后我查看了自己的旧代码,发现其中一些也受此影响。

下面的代码启动并等待两个任务,task1task2,它们几乎相同。 task1task2 的不同之处仅在于它运行一个永无止境的循环。 IMO,这两种情况对于一些执行 CPU 密集型工作的现实场景来说都是非常典型的。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    public class Program
    {
        static async Task TestAsync()
        {
            var ct = new CancellationTokenSource(millisecondsDelay: 1000);
            var token = ct.Token;

            // start task1
            var task1 = Task.Run(() =>
            {
                for (var i = 0; ; i++)
                {
                    Thread.Sleep(i); // simulate work item #i
                    token.ThrowIfCancellationRequested();
                }
            });

            // start task2
            var task2 = Task.Run(() =>
            {
                for (var i = 0; i < 1000; i++)
                {
                    Thread.Sleep(i); // simulate work item #i
                    token.ThrowIfCancellationRequested();
                }
            });  

            // await task1
            try
            {
                await task1;
            }
            catch (Exception ex)
            {
                Console.WriteLine(new { task = "task1", ex.Message, task1.Status });
            }

            // await task2
            try
            {
                await task2;
            }
            catch (Exception ex)
            {
                Console.WriteLine(new { task = "task2", ex.Message, task2.Status });
            }
        }

        public static void Main(string[] args)
        {
            TestAsync().Wait();
            Console.WriteLine("Enter to exit...");
            Console.ReadLine();
        }
    }
}

小提琴is here。输出:

{ 任务 = 任务 1,消息 = 操作已取消。状态 = 已取消 } {任务=任务2,消息=操作被取消。状态=故障}

为什么task1 的状态是Cancelled,而task2 的状态是Faulted 注意,在这两种情况下我都没有通过token 作为Task.Run 的第二个参数。

【问题讨论】:

  • 很高兴我的帖子激起了你的疑问。
  • @l3arnon,这是一个很棒的帖子,事实上,事实上,他们俩都是。

标签: c# .net multithreading task-parallel-library async-await


【解决方案1】:

这里有两个问题。首先,将CancellationToken 传递给Task.Run API 总是一个好主意,除了让它对任务的lambda 可用。这样做会将令牌与任务相关联,并且对于正确传播由 token.ThrowIfCancellationRequested 触发的取消至关重要。

但这并不能解释为什么 task1 的取消状态仍然可以正确传播 (task1.Status == TaskStatus.Canceled),而 task2 (task2.Status == TaskStatus.Faulted) 的取消状态却没有。

现在,这可能是聪明的 C# 类型推理逻辑可以违背开发人员意愿的极少数情况之一。 herehere 对此进行了详细讨论。综上所述,在task1的情况下,编译器会推断出以下Task.Run的覆盖:

public static Task Run(Func<Task> function)

而不是:

public static Task Run(Action action)

那是因为task1 lambda 没有自然的代码路径超出for 循环,所以它也可能是一个Func&lt;Task&gt; lambda,尽管它不是async 并且它没有返回任何东西。这是编译器比Action 更喜欢的选项。那么,Task.Run 的这种覆盖的使用就相当于:

var task1 = Task.Factory.StartNew(new Func<Task>(() =>
{
    for (var i = 0; ; i++)
    {
        Thread.Sleep(i); // simulate work item #i
        token.ThrowIfCancellationRequested();
    }
})).Unwrap();

Task.Factory.StartNew 返回一个Task&lt;Task&gt; 类型的嵌套任务,它通过Unwrap()unwrapped 转换为TaskTask.Runis smart enough 在接受 Func&lt;Task&gt; 时自动进行这种解包。 未包装的 promise 样式任务正确地从其内部任务传播取消状态Func&lt;Task&gt; lambda 将其作为 OperationCanceledException 异常抛出。 task2 不会发生这种情况,它接受 Action lambda 并且不创建任何内部任务。取消不会为 task2 传播,因为 token 尚未通过 Task.Runtask2 关联。

最后,这可能是task1 所期望的行为(当然不是task2),但我们不希望在这两种情况下都在后台创建嵌套任务。此外,task1 的这种行为很容易通过在 for 循环之外引入条件 break 来破坏。

task1 的正确代码应该是这样的

var task1 = Task.Run(new Action(() =>
{
    for (var i = 0; ; i++)
    {
        Thread.Sleep(i); // simulate work item #i
        token.ThrowIfCancellationRequested();
    }
}), token);

【讨论】:

  • 所以当一个令牌被隐式传递 Task.Run 而不是通过参数显式传递时,它是否被视为 lambda 内部发生的所有其他异常?我尝试浏览 CancellationToken 注册,但在源代码中无法弄清楚
  • @YuvalItzchakov,Task.Run 是这样。但是,对于普通的 async Task 方法,魔术是由 async/await 编译器基础结构代码完成的:如果在令牌上请求取消,async Task TestAsync(CancellationToken token) { token.ThrowIfCancellationRequested(); } 将返回带有 task.IsCanceled == trueTask
  • 这很有趣。我想知道为什么他们的行为不同。异步 lambda 的行为会像 task1task2 吗?还是会根据返回委托类型而有所不同?
  • 对。我认为我必须接受这个事实,直到我能理解为什么 MSFT 选择“更好的重载”是Func&lt;Task&gt;
  • @Noseratio 我给 Eric Lippert 发了邮件,请他对 lambda 推理发表评论。他回答:stackoverflow.com/a/24316474/1870803
猜你喜欢
  • 1970-01-01
  • 2020-01-29
  • 1970-01-01
  • 2012-01-28
  • 2014-06-27
  • 2011-04-02
  • 1970-01-01
  • 1970-01-01
  • 2014-09-22
相关资源
最近更新 更多