等待取消任务的简洁方法?

Concise way to await a canceled Task?

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

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

鉴于我请求取消,这是预料之中的,我真的希望忽略这个例外。这似乎是一个常见的情况。

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

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

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

编辑:如评论中所述,如果在等待期间取消任务,这不会有任何好处。


编辑 2:这种方法有点矫枉过正,因为它需要额外的资源——在热路径中这可能会影响性能。 (我正在使用 SemaphoreSlim 但你可以使用另一个 sync.primitive 同样成功)

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

  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
}

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

备注: 它不是 return 的原始任务。因此,如果您需要从中 return 编辑的内容,您应该检查原始任务。

我不认为有任何内置的东西,但您可以在扩展方法中捕获您的逻辑(一个用于 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)。)

我假设 task 所做的一切都使用 CancellationToken.ThrowIfCancellationRequested() 来检查取消。这会在设计上引发异常。

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

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

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 是学习这些东西的绝佳参考。

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

Creates a task that will complete when any of the supplied tasks have completed.

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 参数,因此每次调用都会在堆中分配一个数组。

我的最终解决方案是按照 Matt Johnson-Pint 的建议创建一个扩展方法。但是,我 return 一个布尔值,指示任务是否已取消,如 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 属性.