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

问题描述 投票:0回答:2

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

下面的代码启动并等待两个任务,

task1
task2
,它们几乎相同。
task1
task2
的唯一不同之处在于它运行永无止境的循环。 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();
        }
    }
}

小提琴在这里。输出:

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

为什么

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

c# multithreading async-await task-parallel-library cancellation-token
2个回答
13
投票

这里有两个问题。首先,除了让任务的 lambda 可用之外,将

CancellationToken
传递给
Task.Run
API 始终是一个好主意。这样做会将令牌与任务相关联,对于正确传播由
token.ThrowIfCancellationRequested
触发的取消至关重要。

但这并不能解释为什么

task1
的取消状态仍能正确传播 (
task1.Status == TaskStatus.Canceled
),而
task2
(
task2.Status == TaskStatus.Faulted
) 则不然。

现在,这可能是非常罕见的情况之一,其中巧妙的 C# 类型推断逻辑可能会违背开发人员的意愿。 此处此处对此进行了详细讨论。总而言之,在使用

task1
的情况下,编译器会推断出以下对
Task.Run
的覆盖:

public static Task Run(Func<Task> function)

而不是:

public static Task Run(Action action)

这是因为

task1
lambda 没有脱离
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();

类型为

Task<Task>
的嵌套任务由
Task.Factory.StartNew
返回,它通过 Task
unwrapped
变为
Unwrap()
Task.Run
足够聪明,可以在接受
Func<Task>
时自动进行此类展开。 未包装的 Promise 样式任务正确地从其内部任务传播取消状态,由
OperationCanceledException
lambda 作为
Func<Task>
异常抛出。对于
task2
来说,这种情况不会发生,它接受
Action
lambda 并且不会创建任何内部任务。取消不会传播到
task2
,因为
token
尚未通过
task2
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);

0
投票

该示例也可以在没有取消标记的情况下工作,只需抛出一个OperationCanceledException。结果是相同的 - 任务 1 被取消,而任务 2 出现故障。 使用 async/await 时,任务在抛出 OperationCanceledException 时始终处于取消状态。 对我来说,这不是一贯的行为,因为你不能依赖国家。 只要使用 async/await 模式就没有问题(取消状态和错误状态都会抛出可以捕获的异常)。

© www.soinside.com 2019 - 2024. All rights reserved.