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

时间:2014-06-23 06:37:02

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

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

下面的代码启动并等待两个任务task1task2,它们几乎完全相同。 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();
        }
    }
}

小提琴is here。输出:

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

为什么task1的状态为Cancelled,但task2的状态为Faulted请注意,在这两种情况下我都会< em> not 将token作为Task.Run的第二个参数传递。

1 个答案:

答案 0 :(得分:10)

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

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

现在,这可能是一种非常罕见的情况,其中聪明的C#类型推理逻辑可以与开发人员的意志对抗。我们详细讨论了herehere。总而言之,对于Task.Run,编译器推断出以下public static Task Run(Func<Task> function) 的覆盖:

public static Task Run(Action action)

而不是:

task1

那是因为for lambda没有Func<Task>循环之外的自然代码路径,所以它也可能是async lambda,尽管它不是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<Task>

Task.Factory.StartNew返回Task类型的嵌套任务,Unwrap()unwrapped传递给Task.RunFunc<Task> is smart enough在接受OperationCanceledException时自动进行此类展开。 解包的promise-style任务正确地从其内部任务传播取消状态,由Func<Task> lambda作为task2异常抛出。这不会发生在Action,它接受​​task2 lambda并且不会创建任何内部任务。由于token未通过task2 Task.Runtask1相关联,因此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); 的这种行为。

{{1}}的正确代码应为

{{1}}