简便的方法来等待已取消的任务?

时间:2019-12-20 18:29:30

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

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

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

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

是否有更简洁的方法?我错过了一些取消事宜吗?似乎应该有一个task.CancellationExpected()方法之类的东西。

5 个答案:

答案 0 :(得分:3)

有一个内置的机制,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参数,因此效率略低,因此每次调用时都会在堆中分配一个数组。

答案 1 :(得分:1)

我认为没有内置的东西,但是您可以在扩展方法中捕获逻辑(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)。)

答案 2 :(得分:0)

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

因此您的选择是有限的。如果task是您编写的操作,则可以使其不使用ThrowIfCancellationRequested(),而是检查IsCancellationRequested并在需要时正常结束。但是您知道,如果您执行此操作,则该任务的状态将不会为Canceled

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

答案 3 :(得分:0)

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

答案 4 :(得分:-2)

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

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
}

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

注释: 它不返回原始任务。因此,如果您需要从中退回一些东西,则应该检查原始任务。