CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态 [英] Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested

查看:19
本文介绍了CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

通常我不会发布带有答案的问题,但这次我想吸引一些人注意我认为可能是一个晦涩但常见的问题.它是由这个问题触发的,从那时起我查看了自己的旧代码,发现其中一些也受此影响.

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

使用系统;使用 System.Threading;使用 System.Threading.Tasks;命名空间控制台应用程序{公开课计划{静态异步任务 TestAsync(){var ct = new CancellationTokenSource(millisecondsDelay: 1000);var token = ct.Token;//启动任务 1var task1 = Task.Run(() =>{for (var i = 0; ; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}});//启动任务2var task2 = Task.Run(() =>{for (var i = 0; i <1000; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}});//等待任务 1尝试{等待任务1;}捕获(异常前){Console.WriteLine(new { task = "task1", ex.Message, task1.Status });}//等待任务 2尝试{等待任务2;}捕获(异常前){Console.WriteLine(new { task = "task2", ex.Message, task2.Status });}}public static void Main(string[] args){TestAsync().Wait();Console.WriteLine("回车退出...");Console.ReadLine();}}}

小提琴在这里.输出:

<前>{ task = task1,Message = 操作被取消.,Status = Canceled }{ task = task2,Message = 操作被取消.,Status = Faulted }

为什么task1的状态是Cancelled,而task2的状态是Faulted? 请注意,在这两种情况下,我都token 作为第二个参数传递给 Task.Run.

解决方案

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

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

现在,这可能是巧妙的 C# 类型推断逻辑违背开发人员意愿的极少数情况之一.此处这里.综上所述,对于task1,编译器推断出以下对Task.Run的覆盖:

public static Task Run(Func函数)

而不是:

public static Task Run(Action action)

那是因为 task1 lambda 没有 for 循环之外的自然代码路径,所以它也可能是一个 Func lambda,尽管它不是 async 并且它不返回任何内容.这是编译器比 Action 更喜欢的选项.然后,使用这样的 Task.Run 覆盖相当于:

var task1 = Task.Factory.StartNew(new Func(() =>;{for (var i = 0; ; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}})).解包();

Task 类型的嵌套任务由 Task.Factory.StartNew 返回,它获取 解包TaskUnwrap().Task.Run 很聪明足以在它接受Func时自动进行这种解包.未包装的承诺样式任务正确地传播了其内部任务的取消状态,由 Func lambda 作为 OperationCanceledException 异常抛出.task2 不会发生这种情况,它接受一个 Action lambda 并且不创建任何内部任务.task2 的取消不会传播,因为 token 尚未通过 Task.Runtask2 关联.

最后,这可能是 task1 想要的行为(当然不是 task2),但我们不想在幕后创建嵌套任务无论哪种情况.此外,通过在 for 循环之外引入条件 breaktask1 的这种行为很容易被破坏.

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

var task1 = Task.Run(new Action(() =>{for (var i = 0; ; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}}), 令牌);

Usually I don't post a question with the answer, but this time I'd like to attract some attention to what I think might be an obscure yet common issue. It was triggered by this question, since then I reviewed my own old code and found some of it was affected by this, too.

The code below starts and awaits two tasks, task1 and task2, which are almost identical. task1 is only different from task2 in that it runs a never-ending loop. IMO, both cases are quite typical for some real-life scenarios performing CPU-bound work.

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();
        }
    }
}

The fiddle is here. The output:

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

Why the status of task1 is Cancelled, but the status of task2 is Faulted? Note, in both cases I do not pass token as the 2nd parameter to Task.Run.

解决方案

There are two problems here. First, it's always a good idea to pass CancellationToken to the Task.Run API, besides making it available to the task's lambda. Doing so associates the token with the task and is vital for the correct propagation of the cancellation triggered by token.ThrowIfCancellationRequested.

This however doesn't explain why the cancellation status for task1 still gets propagated correctly (task1.Status == TaskStatus.Canceled), while it doesn't for task2 (task2.Status == TaskStatus.Faulted).

Now, this might be one of those very rare cases where the clever C# type inference logic can play against the developer's will. It's discussed in great details here and here. To sum up, in case with task1, the following override of Task.Run is inferred by compiler:

public static Task Run(Func<Task> function)

rather than:

public static Task Run(Action action)

That's because the task1 lambda has no natural code path out of the for loop, so it may as well be a Func<Task> lambda, despite it is not async and it doesn't return anything. This is the option that compiler favors more than Action. Then, the use of such override of Task.Run is equivalent to this:

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

A nested task of type Task<Task> is returned by Task.Factory.StartNew, which gets unwrapped to Task by Unwrap(). Task.Run is smart enough to do such unwrapping automatically for when it accepts Func<Task>. The unwrapped promise-style task correctly propagates the cancellation status from its inner task, thrown as an OperationCanceledException exception by the Func<Task> lambda. This doesn't happen for task2, which accepts an Action lambda and doesn't create any inner tasks. The cancellation doesn't get propagated for task2, because token has not been associated with task2 via Task.Run.

In the end, this may be a desired behavior for task1 (certainly not for task2), but we don't want to create nested tasks behind the scene in either case. Moreover, this behavior for task1 may easily get broken by introducing a conditional break out of the for loop.

The correct code for task1 should be this:

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

这篇关于CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆