为什么CancellationToken与CancellationTokenSource是分开的?

And*_*sov 119 .net multithreading task-parallel-library cancellationtokensource cancellation-token

我正在寻找除了CancellationTokenSource类之外还引入.NET CancellationToken结构的原因.我的理解是如何的API被使用,但想也明白为什么它就是这样设计的.

即,我们为什么要:

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

...
public void SomeCancellableOperation(CancellationToken token) {
    ...
    token.ThrowIfCancellationRequested();
    ...
}
Run Code Online (Sandbox Code Playgroud)

而不是直接传递CancellationTokenSource,如:

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

...
public void SomeCancellableOperation(CancellationTokenSource cts) {
    ...
    cts.ThrowIfCancellationRequested();
    ...
}
Run Code Online (Sandbox Code Playgroud)

这是一个基于以下事实的性能优化:取消状态检查比传递令牌更频繁吗?

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

鉴于两种情况下没有锁定的挥发性bool就足够了,我仍然无法理解为什么会更快.

谢谢!

Mik*_*ell 89

我参与了这些课程的设计和实施.

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

如果门因行为不当的API而被打开,那么取消设计的有用性很快就会被侵蚀.

  • 非常感谢!内部知识非常感谢. (6认同)
  • “CancellationTrigger”和“CancellationListener”是更好的名称。我想知道是否有办法改变这样的事情。 (3认同)

Sol*_*ogi 79

我有完全相同的问题,我想了解这个设计背后的基本原理.

接受的答案得到了完全正确的理由.以下是设计此功能的团队的确认(强调我的):

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

链接:.NET 4取消框架

在我看来,CancellationToken只能观察状态而不能改变它,这一事实非常关键.你可以像糖果一样分发令牌,从不担心除了你以外的其他人会取消它.它可以保护您免受敌对的第三方代码.是的,机会很小,但我个人喜欢这种保证.

我也觉得它使API更清洁,避免意外错误并促进更好的组件设计.

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

CancellationToken API

CancellationTokenSource API

如果你要组合它们,在编写LongRunningFunction时,我会看到像'Cancel'那样多次重载的方法,我不应该使用它们.就个人而言,我也讨厌看到Dispose方法.

我认为当前的类设计遵循"成功的关键"理念,它指导开发人员创建更好的组件,可以处理任务取消,然后以多种方式将它们组合在一起以创建复杂的工作流程.

我问你一个问题,你有没有想过token.Register的目的是什么?这对我来说没有意义.然后我阅读Managed Threads中的Cancellation,一切都变得清晰.

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

  • 如果您想知道 `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)的引用 (5认同)

Cor*_*son 63

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

它们提供了这种功能分离,使事情更具可预测性:当你传递一个方法时CancellationToken,你知道你仍然是唯一可以取消它的人.当然,该方法仍然可以抛出一个TaskCancelledException,但它CancellationToken本身 - 以及引用相同令牌的任何其他方法 - 将保持安全.

  • 经过进一步思考,我发现自己质疑其合理性。那么,提供 CancellationTokenSource 将实现的 ICancellationToken 接口不是更有意义吗?我希望 .NET 团队的某个人能够插话。 (2认同)

Ern*_*rno 9

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

CancellationTokenSource在源上调用Cancel时设置令牌的所有副本的状态.请参阅此MSDN页面

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