为什么 CancellationToken 要与 CancellationTokenSource 分离?

172

我正在寻找一个解释为什么.NET引入了CancellationToken结构体,除了CancellationTokenSource类。 我理解如何使用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,并且对于每个令牌,取消检查是一个局部字段访问?

考虑到在这两种情况下,没有锁的易失性 bool 变量就足够了,我仍然看不出为什么这样会更快。

谢谢!

5个回答

166

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

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

如果留下了不良API的漏洞,那么取消设计的有用性很快就会被侵蚀。

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

CancellationToken == “取消监听器”


9
非常感谢!内部信息真的很受欢迎。 - Andrey Tarantsov
1
@Mike -- 只是好奇:为什么你不能像在CT上那样在CTS上调用ThrowIfCancellationRequested()? - rory.ap
它们有不同的职责,写上 cts.Token.ThrowIfCancellationRequested() 并不会太啰嗦,因此我们没有直接在 CTS 上添加监听器 API。 - Mike Liddell
如果这个答案能够展示一些API不良行为的例子,那就更好了。 - boatcoder
3
“CancellationTrigger”和“CancellationListener”是更好的名称。我想知道是否有一条路径可以改变这样的东西。 - Vimes
显示剩余3条评论

111

我有同样的问题,我想了解这个设计背后的原理。

被接受的答案完全正确地阐述了原理。以下是设计该功能的团队确认(重点在于我):

两种新类型构成了框架的基础:一个 CancellationToken 是表示“潜在取消请求”的结构体。该结构体作为参数传递给方法调用,方法可以轮询它或注册回调,在取消请求时触发。一个 CancellationTokenSource 是提供初始化取消请求机制的类,它有一个 Token 属性用于获取相关联的标记。将这两个类合并成一个是很自然的选择,但是这个设计将“初始化取消请求”和“观察和响应取消”这两个关键操作清晰地分离开来。特别是只接受 CancellationToken 的方法可以观察取消请求,但不能发起取消请求。

链接:.NET 4 Cancellation Framework

在我看来,CancellationToken只能观察状态而不能改变它的事实非常关键。你可以像发糖果一样分发令牌,而不必担心除了你之外的其他人会取消它。它保护你免受敌对第三方代码的侵害。是的,这种可能性很小,但我个人喜欢这种保证。
我还觉得它使API更清晰,避免了意外错误,并促进了更好的组件设计。
让我们来看看这两个类的公共API。

CancellationToken API

CancellationTokenSource API

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

我认为当前的类设计遵循了“成功之坑”哲学,它引导开发人员创建能够处理Task取消并以多种方式将它们组合在一起以创建复杂工作流的更好组件。

让我问你一个问题,你是否想知道token.Register的目的是什么?对我来说它没有意义。然后我读了管理线程中的取消,一切变得清晰明了。

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


9
如果你想知道 CancellationTokenSource 如何实际上在它关联的 token 上发起取消请求(token 本身无法这样做):CancellationToken 有这个内部构造函数:internal CancellationToken(CancellationTokenSource source) { this.m_source = source; } 和这个属性:public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } } CancellationTokenSource 使用内部构造函数,所以 token 有对源的引用(m_source)。 - chviLadislav
为什么要在你的应用程序中包含“敌对”的第三方代码? - boatcoder

66

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

他们提供这种功能上的分离是为了使事情更加可预测:当您将 CancellationToken 传递给方法时,您知道仍然只有您才能取消它。当然,该方法仍可能抛出 TaskCancelledException,但是 CancellationToken 本身以及任何引用相同令牌的其他方法都会保持安全。


2
经过进一步思考,我开始质疑其可行性。那么,提供ICancellationToken接口并由CancellationTokenSource实现是否更有意义呢?我希望.NET团队中的某个人能够发表意见。 - Andrey Tarantsov
1
这可能仍然会导致聪明的程序员将其转换为CancellationTokenSource。你可能认为可以直接说“不要那样做”,但人们(包括我!)偶尔会采取这些措施来获得一些隐藏的功能,而且这种情况确实会发生。至少这是我的目前理论。 - Cory Nelson
1
这通常不是设计API的方式,除非涉及安全问题。我更愿意押注于其他解释,例如“为将来的性能优化保持选择余地”。但至今为止,你的解释是最好的。 - Andrey Tarantsov
@CoryNelson:你的回答提出了很好的区分。但是,如果您按照Task.Run()帮助中提供的示例代码,线程(作为闭包)可以访问CTS和CT;那里没有保护措施。通过Task.Run(Action,Token)提供令牌实际上并没有将令牌传递给线程 - 我的意思是,签名为Task.Run(Action<Token>, Token)会更有意义吗? - Mike C
@MikeC 是的,将令牌传递到Task.Run中是一种非常罕见的应用,因为它只会在操作开始之前触发令牌时才会取消...通常您总是希望将其传递到操作本身中。 - Cory Nelson
显示剩余3条评论

11

CancellationToken是一个结构体,因为要将它传递给方法,所以可能会存在许多副本。

CancellationTokenSource在调用源上的Cancel时,会设置所有令牌的状态。 参见此MSDN页面

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


1
谢谢。请注意,另一个答案指出,在 IL 中,CancellationTokenSource 技术上并不设置令牌的状态;相反,令牌包装了对 CancellationTokenSource 的实际引用,并简单地访问它以检查取消。如果有什么问题,这里只能失去速度。 - Andrey Tarantsov
+1. 我不明白。如果它是值类型,那么每个方法都有自己的值(单独的值)。那么一个方法如何知道是否已经调用了取消?它没有任何对TokenSource的引用。所有方法都只能看到像“5”这样的本地值类型。你能解释一下吗? - Royi Namir
1
@RoyiNamir:每个 CancellationToken 都有一个类型为 CancellationTokenSource 的私有引用“m_source”。 - Andreyul

2
CancellationTokenSource是发出取消请求的“东西”,无论出于什么原因。它需要一种方式将该取消请求“分发”到其发出的所有CancellationToken。例如,当请求被中止时,ASP.NET可以取消操作。每个请求都有一个CancellationTokenSource,它将取消请求转发到其发出的所有令牌。
顺便说一下,这对单元测试非常有用-创建自己的取消标记源,获取标记,在源上调用Cancel,并将标记传递给必须处理取消请求的代码。

谢谢 - 但是这两个职责(它们非常相关)可以分配给同一个类。并没有真正解释它们为什么要分开。 - Andrey Tarantsov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接