在CancellationToken.ThrowIfCancellationRequested之后,故障状态与取消状态的任务状态是什么?

22

通常我不会发布一个带有答案的问题,但这次我想吸引一些关注,因为我认为这可能是一个模糊但普遍存在的问题。它是由 这个问题触发的,自那时以来,我审查了自己的旧代码,并发现其中一些也受到了影响。

下面的代码启动并等待两个任务task1task2,它们几乎相同。 task1task2 的唯一区别在于它运行一个永不停歇的循环。在我看来,这两种情况对于执行 CPU-bound 工作的某些实际场景非常典型。

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

这里是小提琴 的代码。输出结果:

{ task = task1, Message = 操作已取消,Status = 已取消 }
{ task = task2, Message = 操作已取消,Status = 已故障 }

为什么task1的状态是已取消,而task2的状态是已故障请注意,在两种情况下,我都没有将token作为第二个参数传递给Task.Run


2
我很高兴我的帖子引发了你的问题。 - i3arnon
2
@l3arnon,确实是一篇很棒的帖子,事实上,两篇都是。 - noseratio - open to work
1个回答

13

这里有两个问题。首先,在将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<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.Factory.StartNew 返回的嵌套类型为 Task<Task> 的任务,会通过 Unwrap() 方法被 展开Task。当 Task.Run 接受 Func<Task> 时,它 足够智能 地自动执行这种展开操作。 展开后的 Promise 风格任务可以正确地传播其内部任务的取消状态,由 Func<Task> lambda 抛出的 OperationCanceledException 异常。对于接受 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); // simulate work item #i
        token.ThrowIfCancellationRequested();
    }
}), token);

@YuvalItzchakov,对于Task.Run是正确的。但是,对于普通的async Task方法,魔法是由async/await编译器基础设施代码完成的:如果在令牌上请求取消,则async Task TestAsync(CancellationToken token) { token.ThrowIfCancellationRequested(); }将返回一个带有task.IsCanceled == trueTask - noseratio - open to work
很有趣。我想知道它们为什么会表现不同。异步lambda是否会像task1task2那样行为一致?还是它取决于返回委托类型而有所不同? - Yuval Itzchakov
@YuvalItzchakov,我也是。Task.RunTask.Factory.StartNew()new Task()Task.ContinueWith让我们有一个选项来显式地将令牌与它们创建的任务关联起来。另一方面,async Task方法没有这个选项,需要另一个C#语法扩展来实现。我猜这可能是原因。 - noseratio - open to work
1
好的。我想我必须接受这个事实,直到我能理解为什么微软选择“更好的重载”是 Func<Task> - Yuval Itzchakov
2
@Noseratio,我给Eric Lippert发了一封邮件,请他评论关于lambda推断的问题。他回答说:https://dev59.com/lGAf5IYBdhLWcg3wwk0V#24316474 - Yuval Itzchakov
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接