CancellationToken.ThrowIfCancellationRequested后出现故障与已取消的任务状态

nos*_*tio 17 .net c# multithreading task-parallel-library async-await

通常我不会回答问题,但这次我想引起一些我认为可能是一个模糊而又常见的问题的注意.它是由这个问题引发的,从那以后我查看了我自己的旧代码,发现其中一些也受此影响.

下面的代码开始,等待着两个任务,task1并且task2,这几乎是相同的.task1只是task2因为它运行一个永无止境的循环.对于执行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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

小提琴在这里.输出:

{ task = task1, Message = The operation was canceled., Status = Canceled }
{ task = task2, Message = The operation was canceled., Status = Faulted }

为什么状态task1Cancelled,但状态task2Faulted请注意,在这两种情况下,我都不会将token第二个参数传递给Task.Run.

nos*_*tio 11

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

然而,这并不能解释为什么取消状态task1仍然正确传播(task1.Status == TaskStatus.Canceled),而不是task2(task2.Status == TaskStatus.Faulted).

现在,这可能是一种非常罕见的情况,其中聪明的C#类型推断逻辑可以违背开发人员的意愿.这里这里详细讨论了它.总而言之,如果是,编译器推断出task1以下覆盖Task.Run:

public static Task Run(Func<Task> function)
Run Code Online (Sandbox Code Playgroud)

而不是:

public static Task Run(Action action)
Run Code Online (Sandbox Code Playgroud)

这是因为task1lambda没有自然的代码路径for,所以它也可能是一个Func<Task>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();
Run Code Online (Sandbox Code Playgroud)

类型的嵌套任务Task<Task>是返回Task.Factory.StartNew,这被拆开TaskUnwrap().Task.Run 足够聪明,可以在接受时自动进行此类展开Func<Task>.未展开的promise-style任务正确地从其内部任务传播取消状态,OperationCanceledExceptionFunc<Task>lambda 作为异常抛出.这不会发生task2,它接受Actionlambda并且不会创建任何内部任务.取消不会传播task2,因为token没有与task2via 关联Task.Run.

最后,这可能是一个期望的行为task1(当然不适用于task2),但我们不希望在任何一种情况下在场景后面创建嵌套任务.此外,这种行为task1可以很容易地得到通过引入条件碎break了的for循环.

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

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

  • @Noseratio 我给 Eric Lippert 发了邮件,请他对 lambda 推理发表评论。他回答:http://stackoverflow.com/a/24316474/1870803 (2认同)