为什么CancellationToken与CancellationTokenSource分开?


136

我正在寻找为什么CancellationToken除了CancellationTokenSource类之外还引入了.NET 结构的理由。我了解如何使用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可以跟踪和更新CancellationTokens,对于每个令牌,取消检查是本地访问?

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

谢谢!

Answers:


109

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

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

如果因API行为不佳而将门打开,则取消设计的用处可能很快就会消失。

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

CancellationToken ==“取消监听器”


7
非常感谢!内在的知识真的很感激。
Andrey Tarantsov

stackoverflow.com/questions/39077497/… 您知道StreamSocket的inputstream的默认超时应该是什么吗?当我使用cancellationtoken取消读取操作时,它也会关闭相关的套接字。有什么办法可以解决这个问题?
sam18

@Mike-很好奇:为什么不能像在CT上那样在CTS上调用ThrowIfCancellationRequested()之类的东西?
rory.ap

他们有不同的职责,编写cts.Token.ThrowIfCancellationRequested()不太繁琐,因此我们没有在CTS上直接添加侦听器API。
Mike Liddell

@Mike为什么cancelingtoken是结构而不是类?我看不到任何性能上的优势
Neir0 '18

85

我有确切的问题,我想了解这种设计背后的原理。

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

框架的基础有两种新类型:A CancellationToken是代表“潜在的取消请求”的结构。此结构作为参数传递给方法调用,方法可以对其进行轮询或注册一个在请求取消时要触发的回调。A CancellationTokenSource是提供启动取消请求机制的类,它具有Token 用于获取关联令牌的属性。将这两个类合而为一是很自然的,但是这种设计允许将两个关键操作(发起取消请求与观察并响应取消)完全分开。特别是,仅采用的方法CancellationToken可以观察到取消请求,但不能发起一个取消请求。

链接:.NET 4取消框架

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

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

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

CancellationToken API

CancellationTokenSource API

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

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

让我问一个问题,您是否想知道token.Register的目的是什么?对我来说这没有意义。然后,我读了《托管线程中的取消》,一切都变得清晰起来。

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


2
如果您想知道如何CancellationTokenSource实际对关联的令牌发起取消请求(令牌本身无法执行取消请求):CancellationToken具有此内部构造函数: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }并且此属性:public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }CancellationTokenSource使用内部构造函数,因此令牌引用了来源(m_source)
chviLadislav

65

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

它们提供了功能上的分离,使事情更可预测:当您传递方法a时CancellationToken,您知道您仍然是唯一可以取消该方法的人。当然,该方法仍然可以抛出TaskCancelledException,但是其CancellationToken本身(以及引用同一令牌的任何其他方法)仍将保持安全。


谢谢!我的眨眼反应是,从语义上讲,这是一种可疑的方法,与提供IReadOnlyList相当。不过,听起来确实很合理,因此请接受您的答案。
Andrey Tarantsov 2013年

1
经过进一步思考,我发现自己对合理性表示怀疑。那么,提供CancellationTokenSource将要实现的ICancellationToken接口是否更有意义?我想从.NET团队有人会插入内容。
安德烈Tarantsov

那可能仍然会导致狡猾的程序员投向CancellationTokenSource。您可能以为您可以说“不要那样做”,但是人们(包括我!)偶尔还是会做这些事情以获得某种隐藏的功能,而且这种情况确实会发生。至少那是我目前的理论。
科里·纳尔逊

1
在大多数情况下,除非与安全性相关,否则这不是设计API的方式。我宁愿押注其他解释,例如“为将来的性能优化保留其选项公开”。但是,到目前为止,您是最好的。
Andrey Tarantsov 2013年

嘿,希望您能请我将已接受的答案重新分配给实施人员。虽然你们都说相同的话,但我认为确定的答案是最重要的。
Andrey Tarantsov 2015年

10

CancellationToken是一个结构这么多的副本可能存在由于其传递给方法。

CancellationTokenSource套打电话时令牌的所有副本的状态Cancel来源。看到此MSDN页面

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


1
谢谢。请注意,另一个答案告诉您,从技术上讲(在IL中),CancellationTokenSource不会设置令牌的状态。相反,令牌包装了对CancellationTokenSource的实际引用,并只需对其进行访问以检查是否取消。如果有的话,速度只能在这里失去。
Andrey Tarantsov 2013年

+1。我不明白 如果其为值类型,则每个方法都有其自己的值(单独的值)。那么方法如何知道是否已调用取消?它没有对TokenSource的任何引用。该方法可以看到的只是一个本地值类型,例如“ 5”。你可以解释吗 ?
罗伊·纳米尔

1
@RoyiNamir:每个CancellationToken都有类型为CancellationTokenSource的私有引用“ m_source”
Andreyul,2014年

2

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

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


谢谢-但两者的责任(这非常相关的)可以给予相同的类。并没有真正解释为什么它们是分开的。
Andrey Tarantsov 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.