【问题标题】:TaskCancellationException how to avoid the exception on success control flow?TaskCancellationException 如何避免成功控制流的异常?
【发布时间】:2016-03-21 01:42:33
【问题描述】:

在我们的应用程序中,我们经常使用 async / await 和 Tasks。因此它确实经常使用 Task.Run,​​有时还使用内置的 CancellationToken 支持取消。

public Task DoSomethingAsync(CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        while (true)
        {
            if (cancellationToken.IsCancellationRequested) break;
            //do some work
        }
    }, cancellationToken);
}

如果我现在使用 CancellationToken 取消执行,则执行会在下一个循环开始时停止,或者如果任务根本没有启动,它会引发异常(Task.Run 中的 TaskCanceledException)。现在的问题是为什么 Task.Run 使用 Exception 来控制成功取消,而不是仅仅返回一个已完成的任务。 MS 没有遵守“不要使用异常来控制执行流程”规则是否有任何具体原因?

我怎样才能避免在一个完全无用的 try catch (TaskCancelledException) 块中对每个支持取消(很多)的方法进行装箱?

【问题讨论】:

  • “取消!=成功完成”。您希望Task<string> 取消后返回什么?
  • 使用 cancellationTokenSource.Cancel(); 取消的任务实例将具有 TaskStatus.RanToCompletion 状态,而不是 TaskStatus .Canceled 状态。
  • 您是正确的具有返回值的任务需要例外。没想到。
  • 我已经回答了类似的问题here 不确定这是否重复。尤其是this comment 应该会有所启发。
  • @S.Dav 这取决于任务是使用 ThrowIfCancellationRequested 还是在令牌设置为 true 时简单地返回。

标签: c# async-await task-parallel-library cancellation-token


【解决方案1】:

抛出异常是有目的的,正如社区中的其他人已经指出的那样。

但是,如果您想更好地控制TaskCanceledException 的行为并且仍然将逻辑隔离到一个地方,您可以实现一个扩展方法来扩展处理取消的Task,就像这样 -

  public async Task DoSomethingAsync(CancellationToken cancellationToken)
    {
        await Task.Run(() =>
        {
            while (true)
            {
                if (cancellationToken.IsCancellationRequested) break;
                //do some work
            }
        }).
        WithCancellation(cancellationToken,false); // pass the cancellation token to extension funciton instead to run
    }   



static class TaskCacellationHelper
{
    private struct Void { } // just to support TaskCompletionSource class.


    public static async Task WithCancellation(this Task originalTask,  CancellationToken ct, bool suppressCancellationExcetion)
    {
        // Create a Task that completes when the CancellationToken is canceled
        var cancelTask = new TaskCompletionSource<Void>();
        // When the CancellationToken is canceled, complete the Task
        using (ct.Register(
        t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask))
        {
            // Create a Task that completes when either the original or
            // CancellationToken Task completes
            Task any = await Task.WhenAny(originalTask, cancelTask.Task);
            // If any Task completes due to CancellationToken, throw OperationCanceledException
            if (any == cancelTask.Task)
            {
                //
                if (suppressCancellationExcetion == false)
                {
                    ct.ThrowIfCancellationRequested();
                }
                else
                {
                    Console.WriteLine("Cancelled but exception supressed");
                }
            }
        }
        // await original task. Incase of cancellation your logic will break the while loop            
        await originalTask;
    }
}

【讨论】:

    【解决方案2】:

    好吧,在您的非常简单的场景中,您实际上看不出有什么不同 - 您实际上并没有使用 Task 的结果,并且您不需要通过复杂的调用堆栈传播取消。

    首先,您的Task 可能会返回一个值。操作取消时返回什么?

    其次,您取消的任务之后可能还有其他任务。您可能希望在方便时通过其他任务传播取消。

    异常传播。在这种用法中,任务取消与Thread.Abort 几乎相同 - 当您发出Thread.Abort 时,ThreadAbortException 用于确保您一直放松回到顶部。否则,您的所有方法都必须检查它们调用的每个方法的结果,检查它们是否被取消,并在需要时返回自身——我们已经看到人们会忽略老式 C 中的错误返回值 :)

    最后,任务取消,就像线程中止一样,是一种例外情况。它已经涉及同步、堆栈展开等。

    但是,这并不意味着您必须使用 try-catch 来捕获异常 - 您可以使用任务状态。例如,您可以使用这样的辅助函数:

    public static Task<T> DefaultIfCanceled<T>(this Task<T> @this, T defaultValue = default(T))
    {
      return
        @this.ContinueWith
          (
            t =>
            {
              if (t.IsCanceled) return defaultValue;
    
              return t.Result;
            }
          );
    }
    

    你可以用作什么

    await SomeAsync().DefaultIfCanceled();
    

    当然,应该注意的是,noöne 会强迫您使用这种取消方法——它只是为了方便而提供的。例如,您可以使用自己的放大类型来保留取消信息,并手动处理取消。但是当您开始这样做时,您会发现使用异常处理取消的原因 - 在命令式代码中这样做很痛苦,因此您要么浪费大量精力而没有收获,要么您将切换到更实用的编程方式(来吧,我们有 cookie!*)。

    (*) 免责声明:我们实际上没有 cookie。但你可以自己做!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-02-10
      • 1970-01-01
      • 2015-10-26
      • 2010-09-29
      • 2016-09-15
      • 2016-08-13
      • 2015-12-11
      相关资源
      最近更新 更多