等待取消任务的简洁方法?
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
属性.
我发现自己经常写这样的代码:
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
属性.