Task.ContinueWith 不适用于 OnlyOnCanceled

Task.ContinueWith does not work with OnlyOnCanceled

微软70-483考试参考书中关于CancellationToken的使用,第一种用signal取消的方式是抛异常,然后介绍第二种:

Instead of catching the exception, you can also add a continuation Task that executes only when the Task is canceled. In this Task, you have access to the exception that was thrown, and you can choose to handle it if that’s appropriate. Listing 1-44 shows what such a continuation task would look like

这是列表 1-44:

        Task task = Task.Run(() =>
        {
            while (!token.IsCancellationRequested)
            {
                Console.Write("*");
                Thread.Sleep(1000);
            }
        }, token).ContinueWith((t) =>
        {
            t.Exception.Handle((e) => true);
            Console.WriteLine("You have canceled the task");
        }, TaskContinuationOptions.OnlyOnCanceled);

这是我完整的主要方法代码:

static void Main(string[] args)
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    var token = cancellationTokenSource.Token;

    Task task = Task.Run(() =>
    {
        while (!token.IsCancellationRequested)
        {
            Console.Write("*");
            Thread.Sleep(1000);
        }
    }, token).ContinueWith((t) =>
    {
        t.Exception.Handle((e) => true);
        Console.WriteLine("You have canceled the task");
    }, TaskContinuationOptions.OnlyOnCanceled);

    Console.ReadLine();
    cancellationTokenSource.Cancel();
    task.Wait();

    Console.ReadLine();
}

然而,与所说的不同,当我按 Enter 时,异常 (AggregationException) 仍然抛给 task.Wait() 调用的 Main 方法。此外,如果我删除该调用,则第二个 Task 永远不会运行(不会抛出异常)。我做错了什么吗?是否可以在不使用 try-catch 的情况下处理异常?

试试这个:

        Task task = Task.Run(() =>
        {
            while (true) {
                token.ThrowIfCancellationRequested();
                Console.Write("*");
                Thread.Sleep(1000);
            }
        }, token).ContinueWith((t) =>
        {
            //t.Exception.Handle((e) => true); //there is no exception
            Console.WriteLine("You have canceled the task");
        }, TaskContinuationOptions.OnlyOnCanceled);

使用 cancellationTokenSource.Cancel() 取消的任务实例将具有 TaskStatus.RanToCompletion 状态,而不是 TaskStatus.Canceled 状态。所以我认为您必须将 TaskContinuationOptions.OnlyOnCanceled 更改为 TaskContinuationOptions.OnlyOnRanToCompletion

您可以在 MSDN 上查看 Task Cancellation 了解更多详情。

这是示例代码:

 CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;

Task task = Task.Run(() =>
{
    while (!token.IsCancellationRequested)
    {
        Console.Write("*");
        Thread.Sleep(1000);
    }
}, token).ContinueWith((t) =>
{
    t.Exception.Handle((e) => true);
    Console.WriteLine("You have canceled the task");
}, TaskContinuationOptions.OnlyOnRanToCompletion);

Console.ReadLine();
cancellationTokenSource.Cancel();

try
    {
        task.Wait();
    }
catch (AggregateException e)
    {
        foreach (var v in e.InnerExceptions)
            Console.WriteLine(e.Message + " " + v.Message);
    }
Console.ReadLine();

要明确说明问题,您的第二个延续未执行,但您认为应该执行:

static void Main(string[] args)
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    var token = cancellationTokenSource.Token;

    Task task = Task.Run(() =>
    {
        while (!token.IsCancellationRequested)
        {
            Console.Write("*");
            Thread.Sleep(1000);
        }
    }, token).ContinueWith((t) =>
    {                                                     //  THIS
        t.Exception.Handle((e) => true);                  //  ISN'T
        Console.WriteLine("You have canceled the task");  //  EXECUTING
    }, TaskContinuationOptions.OnlyOnCanceled);

    Console.ReadLine();
    cancellationTokenSource.Cancel();
    task.Wait();

    Console.ReadLine();
}

第二个继续未执行,因为您必须使用 token.ThrowIfCancellationRequested() 才能触发它:

        Task task = Task.Run(() =>
        {
            while (true)
            {
                token.ThrowIfCancellationRequested();  // <-- NOTICE
                Console.Write("*");
                Thread.Sleep(1000);
            }
        }, token).ContinueWith((t) =>
        {
            Console.WriteLine("From Continuation: " + t.Status);

            Console.WriteLine("You have canceled the task");
        }, TaskContinuationOptions.OnlyOnCanceled);

// OUTPUT:
// ***
// From Continuation: Canceled
// You have canceled the task

调用了第二个延续 ,因为 task.StatusCanceled。下一个片段不会触发第二次继续,因为 task.Status 未设置为 Canceled:

        Task task = Task.Run(() =>
        {
            while (!token.IsCancellationRequested)
            {
                Console.Write("*");
                Thread.Sleep(1000);
            }
        }, token).ContinueWith((t) =>
        {
            Console.WriteLine("From Continuation: " + t.Status);

            Console.WriteLine("You have canceled the task");
        }, TaskContinuationOptions.OnlyOnCanceled);

// OUTPUT:
// AggregationException

如前所述,未调用第二个延续。让我们通过删除 OnlyOnCanceled 子句来强制执行它:

        Task task = Task.Run(() =>
        {
            while (!token.IsCancellationRequested)
            {
                Console.Write("*");
                Thread.Sleep(1000);
            }
        }, token).ContinueWith((t) =>
        {
            Console.WriteLine("From Continuation: " + t.Status);
            Console.WriteLine("You have NOT canceled the task");

        });   // <-- OnlyOnCanceled is gone!

// OUTPUT:
// ***
// From Continuation: RanToCompletion
// You have NOT canceled the task
// (no AggregationException thrown)

请注意,即使调用了 .Cancel(),延续中的 task.Status 也是 RanToCompletion。还要注意没有 AggregationException 被抛出。这表明 仅从令牌源调用 .Cancel() 不会将任务状态设置为 Canceled


当只调用.Cancel()而不调用.ThrowIfCancellationRequested()时,AggregationException实际上是任务取消成功的标志。引用 MSDN article:

If you are waiting on a Task that transitions to the Canceled state, a System.Threading.Tasks.TaskCanceledException exception (wrapped in an AggregateException exception) is thrown. Note that this exception indicates successful cancellation instead of a faulty situation. Therefore, the task's Exception property returns null.

这让我得出了一个宏大的结论:

清单 1-44 是一个 known error.

你的 t.Exception... 行已从我的所有代码中省略,因为“任务的 Exception 属性 returns null " 成功取消后。 should 行已从清单 1-44 中省略。看起来他们将以下两种技术混为一谈:

  1. 我回答的第一段是取消任务的有效方法。 OnlyOnCanceled 延续被调用并且没有抛出异常。
  2. 我的回答的第二个片段也是取消任务的有效方法,但未调用 OnlyOnCanceled 延续,并抛出 AggregationException 供您在 Task.Wait()[=76 处处理=]

免责声明:这两个片段都是取消任务的有效方法,但它们可能存在我不知道的行为差异。