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

5

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

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

鉴于我请求取消,这是可以预料的,我真的希望忽略此异常。这似乎是一个常见情况。

有没有更简洁的方法来处理这个问题?我是否遗漏了有关取消的某些内容?看起来应该有一个 task.CancellationExpected() 方法或类似的东西。


2
如果你已经取消了它,为什么还想要await它呢? - DavidG
4
要求取消,但没有被取消。 - user12447201
5
我假设需要确保任务已经停止运行才能继续进行。这是一个有效的要求。 - Gabriel Luci
2
大多数形式为“我一直在复制粘贴这段代码,如何使其更简洁?”的问题的答案是编写一个包含复制粘贴代码的方法并调用它。您是否拒绝了此解决方案?如果是,能否说出您拒绝的原因? - Eric Lippert
3
@EricLippert 我没有拒绝它。那很可能是答案。我提出问题的原因是,这似乎很常见,所以框架中应该有相应的东西来处理它。 - Jeff Walker Code Ranger
显示剩余10条评论
6个回答

5

有一个内置机制,即使用单个参数的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参数,因此在每次调用时都会在堆上分配一个数组,所以它略微低效。


对于任何感兴趣的人,这里有一个相关的API提案在GitHub上:支持等待任务而不抛出异常 - Theodor Zoulias
我已经在这里发布了基本相同的答案,并添加了一些关于TaskScheduler.UnobservedTaskException事件的注释。 - Theodor Zoulias

2

我认为这个功能没有内置的实现,但是你可以使用扩展方法来捕捉您的逻辑(分别用于TaskTask<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)。)

此外请注意,没有任何内容将其绑定到特定的取消标记或取消标记源。即使您将令牌传递给返回任务的方法,也没有任何固有的绑定该任务到该令牌。因此,您仍然需要为每个调用指定行为。 - Matt Johnson-Pint

1
我假设任何任务正在执行的操作都使用CancellationToken.ThrowIfCancellationRequested()检查取消。这是设计上的异常。
因此,你的选择有限。如果任务是你编写的操作,你可以使它不使用ThrowIfCancellationRequested(),而是在需要时检查IsCancellationRequested并优雅地结束。但是,你知道,如果这样做,任务的状态将不会是Canceled
如果它使用的是你没有编写的代码,则你别无选择。你必须捕获异常。如果你想要避免重复代码(Matt的答案),你可以使用扩展方法。但你必须在某个地方捕获它。

1

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";
    }
  }
}

注意注释并注意程序的三个输出都是可能的。

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

因此,基本上,您的代码是正确的,您正在以应该的方式处理取消。

这本书是学习这些东西的绝佳参考资料。


0

我的最终解决方案是创建一个扩展方法,正如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属性类似。


-3

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

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

编辑:如评论中所述,如果在等待期间任务被取消,则此操作无效。


编辑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
}

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

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


3
这并不意味着 await task 不会抛出异常。当你请求取消时,任务不会立即变为 Canceled - Gabriel Luci
@GabrielLuci 百分之百正确。 - vasil oreshenski
我认为你不需要使用SemphoreSlim或任何同步原语来实现这个。我喜欢你返回一个布尔值来指示是否已取消。 - Jeff Walker Code Ranger
@JeffWalkerCodeRanger 是的,我同意你的观点。我认为Task.WhenAny是最优雅的解决方案。 - vasil oreshenski

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