我发现自己经常写这样的代码:
try
{
cancellationTokenSource.Cancel();
await task.ConfigureAwait(false); // this is the task that was cancelled
}
catch(OperationCanceledException)
{
// Cancellation expected and requested
}
鉴于我请求取消,这是预料之中的,并且我真的希望忽略该异常。这似乎很常见。
是否有更简洁的方法?我错过了一些取消事宜吗?似乎应该有一个task.CancellationExpected()
方法之类的东西。
答案 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)
我认为没有内置的东西,但是您可以在扩展方法中捕获逻辑(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)
。)
答案 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通常用于限制对某些资源的访问(昂贵),但在这种情况下,我将使用它来等待原始任务完成。
注释: 它不返回原始任务。因此,如果您需要从中退回一些东西,则应该检查原始任务。