.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
来传达三个主要事件: ApplicationStarted
、 ApplicationStopping
和ApplicationStopped
:
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
中并被输入到底层子流程中。
IHostedService
的StopAsync
方法就是这种用法的一个很好的例子:
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
来启动进程而不是停止它们。
考虑以下:
- 创建一个
RandomWorker
类。 -
RandomWorker
应该有一个执行一些随机工作的DoWorkAsync
方法。 -
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 测试的优雅实现。
进一步阅读 Toptal 工程博客:
- .NET Core:走向狂野和开源。 微软,你怎么花了这么长时间?!
- 使用 ASP.NET Core 构建 ASP.NET Web API
- 如何引导和创建 .NET 项目