【问题标题】:Concise way to await a canceled Task?等待取消任务的简洁方法?
【发布时间】:2020-04-13 05:44:23
【问题描述】:

我发现自己经常写这样的代码:

try
{
    cancellationTokenSource.Cancel();
    await task.ConfigureAwait(false); // this is the task that was cancelled
}
catch(OperationCanceledException)
{
    // Cancellation expected and requested
}

鉴于我请求取消,这是意料之中的,我真的希望忽略该异常。这似乎很常见。

有没有更简洁的方法来做到这一点?我错过了一些关于取消的事情吗?好像应该有task.CancellationExpected()方法什么的。

【问题讨论】:

  • 如果你取消了它,你为什么还要await它呢?
  • 请求取消,没有取消。
  • @DavidG 我假设在继续之前确保任务实际上已经停止了它的工作。这是一个有效的要求。
  • 大多数问题的答案“我经常复制粘贴此代码,如何使其更简洁?”是编写一个包含复制粘贴代码的方法并调用它。你是否拒绝过这个解决方案,如果是,你能说出你拒绝它的原因吗?
  • @EricLippert 我没有拒绝它。这很可能就是答案。我的问题的根源是,这似乎很常见,以至于框架中会有一些东西。

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


【解决方案1】:

有一个内置机制,Task.WhenAny 方法与单个参数一起使用,但不是很直观。

创建一个在任何提供的任务完成后完成的任务。

await Task.WhenAny(task); // await the task ignoring exceptions
if (task.IsCanceled) return; // the task is completed at this point
var result = await task; // can throw if the task IsFaulted

这并不直观,因为Task.WhenAny 通常与至少两个参数一起使用。此外,由于该方法接受 params Task<TResult>[] tasks 参数,因此效率稍低,因此每次调用时都会在堆中分配一个数组。

【讨论】:

    【解决方案2】:

    我认为没有任何内置功能,但您可以在扩展方法中捕获您的逻辑(一种用于Task,一种用于Task<T>):

    public static async Task IgnoreWhenCancelled(this Task task)
    {
        try
        {
            await task.ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
        }
    }
    
    public static async Task<T> IgnoreWhenCancelled<T>(this Task<T> task)
    {
        try
        {
            return await task.ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            return default;
        }
    }
    

    然后你可以更简单地编写你的代码:

    await task.IgnoreWhenCancelled();
    

    var result = await task.IgnoreWhenCancelled();
    

    (根据您的同步需求,您可能仍想添加.ConfigureAwait(false)。)

    【讨论】:

    • 另请注意,没有任何东西将此绑定到任何特定的取消令牌或取消令牌源。即使您将令牌传递给返回任务的方法,也没有任何东西可以将该任务固有地绑定到该令牌。所以你仍然需要指定每次调用的行为。
    【解决方案3】:

    我假设task 所做的任何事情都使用CancellationToken.ThrowIfCancellationRequested() 来检查取消。这会引发设计异常。

    所以你的选择是有限的。如果task 是您编写的操作,您可以让它不使用ThrowIfCancellationRequested(),而是检查IsCancellationRequested 并在需要时优雅地结束。但如您所知,如果您这样做,任务的状态将不会是Canceled

    如果它使用不是您编写的代码,那么您别无选择。您必须捕获异常。如果需要,您可以使用扩展方法来避免重复代码(马特的回答)。但你必须在某个地方抓住它。

    【讨论】:

      【解决方案4】:

      C# 中可用的取消模式称为协作取消。

      这基本上意味着,为了取消任何操作,应该有两个需要协作的参与者。其中一个是请求取消的参与者,另一个是监听取消请求的参与者。

      为了实现这个模式,您需要一个CancellationTokenSource 的实例,这是一个您可以用来获取CancellationToken 实例的对象。在CancellationTokenSource 实例上请求取消并传播到CancellationToken

      以下代码向您展示了这种模式的实际效果,并希望能澄清您对取消的疑问:

      using System;
      using System.Threading;
      using System.Threading.Tasks;
      
      namespace ConsoleApp2
      {
        public static class Program
        {
          public static async Task Main(string[] args)
          {
            using (var cts = new CancellationTokenSource())
            {
              CancellationToken token = cts.Token;
      
              // start the asyncronous operation
              Task<string> getMessageTask = GetSecretMessage(token);
      
              // request the cancellation of the operation
              cts.Cancel();
      
      
              try
              {
                string message = await getMessageTask.ConfigureAwait(false);
                Console.WriteLine($"Operation completed successfully before cancellation took effect. The message is: {message}");
              }
              catch (OperationCanceledException)
              {
                Console.WriteLine("The operation has been canceled");
              }
              catch (Exception)
              {
                Console.WriteLine("The operation completed with an error before cancellation took effect");
                throw;
              }
      
            }
          }
      
          static async Task<string> GetSecretMessage(CancellationToken cancellationToken)
          {
            // simulates asyncronous work. notice that this code is listening for cancellation
            // requests
            await Task.Delay(500, cancellationToken).ConfigureAwait(false);
            return "I'm lost in the universe";
          }
        }
      }
      

      注意注释,注意程序的3个输出都是可能的。

      无法预测其中哪一个会是实际的程序结果。 关键是,当您等待任务完成时,您不知道实际会发生什么。操作可能在取消生效之前成功或失败,也可能在操作完成之前可以观察到取消请求,也可能因错误而失败。从调用代码的角度来看,所有这些结果都是可能的,你无法猜测。您需要处理所有情况。

      所以,基本上,您的代码是正确的,并且您正在按照应有的方式处理取消。

      This book 是学习这些东西的绝佳参考。

      【讨论】:

        【解决方案5】:

        我的最终解决方案是按照 Matt Johnson-Pint 的建议创建一个扩展方法。但是,我返回一个布尔值,指示任务是否已取消,如 Vasil Oreshenski 的回答所示。

        public static async Task<bool> CompletionIsCanceledAsync(this Task task)
        {
            if (task.IsCanceled) return true;
            try
            {
                await task.ConfigureAwait(false);
                return false;
            }
            catch (OperationCanceledException)
            {
                return true;
            }
        }
        

        此方法已经过全面的单元测试。我选择的名称类似于 ParallelExtensionsExtras 示例代码中的 WaitForCompletionStatus() 方法和 IsCanceled 属性。

        【讨论】:

          【解决方案6】:

          如果您希望在等待之前取消任务,则应检查取消令牌源的状态。

          if (cancellationTokenSource.IsCancellationRequested == false) 
          {
              await task;
          }
          

          编辑:如 cmets 中所述,如果任务在等待期间被取消,这将没有任何好处。


          编辑 2:这种方法过于矫枉过正,因为它会获取额外的资源 - 在热路径中,这可能会影响性能。 (我正在使用SemaphoreSlim,但您可以使用另一个同步原语,同样成功)

          这是对现有任务的扩展方法。如果原始任务被取消,扩展方法将返回包含信息的新任务。

            public static async Task<bool> CancellationExpectedAsync(this Task task)
              {
                  using (var ss = new SemaphoreSlim(0, 1))
                  {
                      var syncTask = ss.WaitAsync();
                      task.ContinueWith(_ => ss.Release());
                      await syncTask;
          
                      return task.IsCanceled;
                  }
              }
          

          这是一个简单的用法:

          var cancelled = await originalTask.CancellationExpectedAsync();
          if (cancelled) {
          // do something when cancelled
          }
          else {
          // do something with the original task if need
          // you can acccess originalTask.Result if you need
          }
          

          工作原理: 总体而言,它等待原始任务完成,如果被取消则返回信息。 SemaphoraSlim 通常用于限制对某些资源的访问(昂贵),但在这种情况下,我使用它来等待原始任务完成。

          注意事项: 它不返回原始任务。因此,如果您需要从它返回的东西,您应该检查原始任务。

          【讨论】:

          • 这并不意味着await task 不会抛出异常。当您请求取消时,任务不会立即转到 Canceled
          • @GabrielLuci 100% 正确。
          • 我认为您不需要 SemphoreSlim 或任何同步原语来实现这一点。我很喜欢你返回一个布尔值来指示它是否被取消。
          • @JeffWalkerCodeRanger 是的,我同意你的看法。我认为 Task.WhenAny 是最优雅的解决方案。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-10-25
          • 1970-01-01
          • 2016-10-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多