.NET 程序员 CancellationToken 指南

已发表: 2022-08-23

有时取消是件好事。 在我的许多 .NET 项目中,我有足够的动力取消内部和外部流程。 微软了解到,开发人员正在通过各种复杂的实现来处理这个常见的用例,并决定必须有更好的方法。 因此,引入了一种常见的取消通信模式CancellationToken ,它是使用较低级别的多线程和进程间通信结构构建的。 作为我对这种模式的初步研究的一部分——在挖掘了微软实现的实际 .NET 源代码之后——我发现CancellationToken可以解决更广泛的问题:订阅应用程序的运行状态、使用不同的操作超时触发器,以及通过标志进行的一般进程间通信。

预期的 CancellationToken 用例

CancellationToken是在 .NET 4 中引入的,作为增强和标准化用于取消操作的现有解决方案的一种手段。 流行的编程语言倾向于实现四种处理取消的通用方法:

告诉,不要接受否定的答案礼貌地询问,并接受拒绝礼貌地设置标志,让它轮询
方法硬停; 稍后解决不一致的地方告诉它停止,但让它清理一切一个直接但温和的停止请求要求它停止,但不要强迫它
概括通往腐败和痛苦的必经之路允许干净的停止点,但必须停止允许干净的停止点,但取消请求可能会被忽略通过标志请求取消
线程pthread_kill ,
pthread_cancel (异步)
pthread_cancel (延迟模式) 不适用通过一面旗帜
。网Thread.Abort 不适用Thread.Interrupt 通过CancellationToken中的标志
爪哇Thread.destroy ,
Thread.stop
不适用Thread.interrupt 通过标志或Thread.interrupted
Python PyThreadState_SetAsyncExc 不适用asyncio.Task.cancel 通过一面旗帜
指导不可接受; 避免这种方法可接受,尤其是当一种语言不支持异常或展开时如果语言支持,可以接受更好,但更多的是集体努力
取消方法摘要和语言示例

CancellationToken位于最后一个类别,取消对话是合作的。

在 Microsoft 推出CancellationToken后,开发社区迅速接受了它,特别是因为许多主要的 .NET API 已更新为原生使用这些令牌。 例如,从 ASP.NET Core 2.0 开始,操作支持可选的CancellationToken参数,该参数可能会在 HTTP 请求已关闭时发出信号,从而允许取消任何操作,从而避免不必要的资源使用。

在深入了解 .NET 代码库之后,很明显CancellationToken的用途不仅限于取消。

显微镜下的 CancellationToken

当更仔细地查看CancellationToken的实现时,我们看到它只是一个简单的标志(即ManualResetEvent )和提供监视和更改该标志的能力的支持基础设施。 CancellationToken的主要实用程序就在它的名字中,这表明这是取消操作的常用方法。 如今,任何具有异步或长时间运行操作的 .NET 库、包或框架都允许通过这些令牌进行取消。

CancellationToken可以通过手动将其标志设置为“true”或在经过一定时间跨度后将其更改为“true”来触发。 不管CancellationToken是如何被触发的,监控这个令牌的客户端代码可以通过以下三种方法之一来确定令牌标志的值:

  • 使用WaitHandle
  • 轮询CancellationToken的标志
  • 当标志的状态通过编程订阅更新时通知客户端代码

在对 .NET 代码库进行进一步研究之后,很明显 .NET 团队发现CancellationTokens在其他与取消无关的场景中很有用。 让我们探索其中一些高级和非品牌用例,它们使 C# 开发人员能够通过多线程和进程间协调来简化复杂的情况。

高级活动的 CancellationTokens

在编写 ASP.NET Core 应用程序时,我们有时需要知道我们的应用程序何时启动,或者我们需要将代码注入到主机关闭过程中。 在这些情况下,我们使用IHostApplicationLifetime接口(以前的IApplicationLifetime )。 此接口(来自 .NET Core 的存储库)使用CancellationToken来传达三个主要事件: ApplicationStartedApplicationStoppingApplicationStopped

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }

乍一看, CancellationToken似乎不属于这里,尤其是因为它们被用作事件。 然而,进一步的检查表明这些标记非常合适:

  • 它们很灵活,允许接口的客户端以多种方式监听这些事件。
  • 它们是开箱即用的线程安全的。
  • 它们可以通过组合CancellationToken从不同的来源创建。

尽管CancellationToken并不适合所有事件需求,但它们非常适合仅发生一次的事件,例如应用程序启动或停止。

CancellationToken 超时

默认情况下,ASP.NET 给我们的关闭时间很少。 在我们需要更多时间的情况下,使用内置的HostOptions类允许我们更改此超时值。 在下面,这个超时值被包装在一个CancellationToken中并被输入到底层子流程中。

IHostedServiceStopAsync方法就是这种用法的一个很好的例子:

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }

IHostedService接口定义中可以明显看出, StopAsync方法采用一个CancellationToken参数。 与该参数相关的注释清楚地传达了 Microsoft 对CancellationToken的最初意图是作为超时机制而不是取消过程。

在我看来,如果这个接口在CancellationToken存在之前就已经存在,那么这可能是一个TimeSpan参数——表示允许处理停止操作多长时间。 根据我的经验,超时场景几乎总是可以转换为具有强大附加效用的CancellationToken

暂时,让我们忘记我们知道StopAsync方法是如何设计的,而是考虑我们将如何设计该方法的协定。 首先让我们定义需求:

  • StopAsync方法必须尝试停止服务。
  • StopAsync方法应该具有正常的停止状态。
  • 无论是否达到正常停止状态,托管服务都必须有一个最长的停止时间,由我们的超时参数定义。

通过任何形式的StopAsync方法,我们满足了第一个要求。 其余的要求很棘手。 CancellationToken通过使用标准的基于 .NET 标志的通信工具来支持对话,从而完全满足这些要求。

CancellationToken 作为通知机制

CancellationToken背后最大的秘密是它只是一个标志。 让我们说明如何使用CancellationToken来启动进程而不是停止它们。

考虑以下:

  1. 创建一个RandomWorker类。
  2. RandomWorker应该有一个执行一些随机工作的DoWorkAsync方法。
  3. DoWorkAsync方法必须允许调用者指定何时开始工作。
 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

上面的类满足前两个要求,剩下第三个。 我们可以使用几个替代接口来触发我们的工作人员,例如时间跨度或简单的标志:

 # With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();

这两种方法都很好,但没有什么比使用CancellationToken更优雅了:

 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

此示例客户端代码说明了此设计的强大功能:

 using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }

CancellationTokenSource将在幕后创建我们的CancellationToken并协调所有相关进程的触发。 在这种情况下,关联的进程是我们的RandomWorker ,它正在等待启动。 这种方法允许我们利用嵌入到默认CancellationToken实现中的线程安全性。

一个扩展的 CancellationToken 工具箱

这些示例演示了CancellationToken如何提供在其预期用例之外有用的解决方案工具箱。 这些工具可以在许多涉及基于标志的进程间通信的场景中派上用场。 无论我们面对的是超时、通知还是一次性事件,我们都可以依靠这个经过 Microsoft 测试的优雅实现。

从上到下依次出现“Gold”(金色)、“Microsoft”和“Partner”(均为黑色)字样,后面是 Microsoft 徽标。
作为 Microsoft 金牌合作伙伴,Toptal 是您的 Microsoft 专家精英网络。 与您需要的专家一起建立高绩效团队 - 随时随地,在您需要的时候!

进一步阅读 Toptal 工程博客:

  • .NET Core:走向狂野和开源。 微软,你怎么花了这么长时间?!
  • 使用 ASP.NET Core 构建 ASP.NET Web API
  • 如何引导和创建 .NET 项目