为什么CancellationToken与CancellationTokenSource分开?

问题描述 投票:117回答:6

我正在寻找为什么要在CancellationTokenSource类之外引入.NET CancellationToken结构的理由。我了解如何使用该API,但也想了解为什么以这种方式设计。

即,为什么我们要拥有:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts.Token);

...
public void SomeCancellableOperation(CancellationToken token) {
    ...
    token.ThrowIfCancellationRequested();
    ...
}

而不是像这样直接传递CancellationTokenSource:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts);

...
public void SomeCancellableOperation(CancellationTokenSource cts) {
    ...
    cts.ThrowIfCancellationRequested();
    ...
}

这是基于取消状态检查比传递令牌更频繁的事实进行的性能优化吗?

以便CancellationTokenSource可以跟踪和更新CancellationToken,并且对于每个令牌,取消检查是对本地字段的访问?

鉴于在两种情况下没有锁定的挥发性布尔变量就足够了,我仍然不明白为什么这样做会更快。

谢谢!

.net multithreading task-parallel-library cancellationtokensource cancellation-token
6个回答
86
投票

我参与了这些类的设计和实现。

简短的答案是“关注点分离”。确实有多种实现策略,至少在类型系统和初始学习方面,有些策略更简单。但是,CTS和CT旨在用于许多场景(例如深层库堆栈,并行计算,异步等),因此在设计时考虑了许多复杂的用例。该设计旨在鼓励成功的图案并阻止反图案而不牺牲性能。

如果由于API行为不佳而让门敞开,那么取消设计的用处就会很快消失。

CancellationTokenSource ==“取消触发”,并生成链接的侦听器

CancellationToken ==“取消监听器”


79
投票

我有完全相同的问题,我想了解这种设计的基本原理。

接受的答案完全正确。这是设计此功能的团队的确认(重点是我的):

两种新类型构成了框架的基础:CancellationToken是代表“潜在的取消请求”的结构。这个struct作为参数传递给方法调用,该方法可以对其进行轮询或注册要在取消时触发的回调请求。 CancellationTokenSource是提供以下内容的类:发起取消请求的机制,它具有令牌用于获取关联令牌的属性。这本来是自然的将这两个类合并为一个,但是此设计允许两个关键操作(发起取消请求与观察和应对取消)进行彻底隔离。尤其是,仅采用CancellationToken的方法可以观察到取消请求,但无法启动。

链接:.NET 4 Cancellation Framework

我认为,CancellationToken只能观察状态而不能更改状态这一事实非常关键。您可以像糖果一样分发令牌,而不必担心除您之外的其他人会取消令牌。它可以保护您免受恶意第三方代码的侵害。是的,机会很小,但我个人很喜欢这种保证。

我也认为它可以使API更加清洁,并避免意外错误,并促进更好的组件设计。

让我们看一下这两个类的公共API。

<< img src =“ https://image.soinside.com/eyJ1cmwiOiAiaHR0cHM6Ly9pLnN0YWNrLmltZ3VyLmNvbS9KOHhEWC5wbmcifQ==” alt =“ CancellationToken API”>

<< img src =“ https://image.soinside.com/eyJ1cmwiOiAiaHR0cHM6Ly9pLnN0YWNrLmltZ3VyLmNvbS9TM3RzTi5wbmcifQ==” alt =“ CancellationTokenSource API”>

如果将它们组合在一起,则在编写LongRunningFunction时,我会看到诸如我不应该使用的'Cancel'的多个重载之类的方法。就个人而言,我也不想看到Dispose方法。

我认为当前的类设计遵循“成功秘诀”的哲学,它指导开发人员创建更好的组件,这些组件可以处理任务取消,然后以多种方式将它们组合在一起以创建复杂的工作流程。

让我问你一个问题,你想知道令牌的目的是什么。注册?这对我来说没有意义。然后我读了Cancellation in Managed Threads,一切变得清晰起来。

我相信TPL中的取消框架设计绝对是一流的。


63
投票

它们是分开的,不是出于技术原因,而是出于语义原因。如果查看ILSpy下CancellationToken的实现,您会发现它只是CancellationTokenSource的包装(因此,在性能方面与传递引用没有什么不同)。

它们提供了这种功能上的分离,使事情更容易预测:当您将CancellationToken方法传递给您时,您知道您仍然是唯一可以取消该方法的人。当然,该方法仍然可以抛出TaskCancelledException,但是CancellationToken本身-以及引用相同标记的任何其他方法-仍然是安全的。


9
投票

CancelationToken是一个结构,由于将其传递给方法,因此可能存在许多副本。

CancellationTokenSource设置在源上调用Cancel时令牌的所有副本的状态。 See this MSDN page

设计的原因可能只是关注点分离和结构速度问题。


1
投票

CancellationTokenSource是出于任何原因发出取消的“事物”。它需要一种方法来将取消操作“分发”到它已发出的所有CancellationToken。例如,这就是当请求中止时ASP.NET可以取消操作的方式。每个请求都有一个CTSource,可将取消转发到已发出的所有令牌。

这非常适合进行单元测试BTW-创建您自己的取消令牌源,获取令牌,在源上调用Cancel,并将令牌传递给必须处理取消的代码。


0
投票

我的“理论”是,CancellationToken的作用是提供线程安全的包装程序,以避免争用

[如果只有一个类,那么一个唯一的实例将与引用的副本一起使用,并且您有很多线程注册处理程序以进行取消,例如,此类应使用某种锁定来管理并发,从而降低性能。

而使用CancellationToken结构,则每个线程都有每个回调队列的副本,因此没有争用。

这纯粹是猜测,我可能完全错了,并且/或者可能有其他很好的理由进行此设计。

唯一可以确定的是,至少有一个很好的理由,可惜的是这种决定没有得到更多的记录,因为使用技术是一种宝贵的见解。

© www.soinside.com 2019 - 2024. All rights reserved.