为什么CancellationToken与CancellationTokenSource是分开的?

时间:2013-01-08 13:14:46

标签: .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();
    ...
}

而不是直接传递CancellationTokenSource,如:

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

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

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

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

鉴于在两种情况下都没有锁定的挥发性bool就足够了,我仍然不明白为什么会更快。

谢谢!

6 个答案:

答案 0 :(得分:82)

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

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

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

答案 1 :(得分:75)

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

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

  

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

链接:.NET 4 Cancellation Framework

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

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

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

CancellationToken API

CancellationTokenSource API

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

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

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

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

答案 2 :(得分:63)

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

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

答案 3 :(得分:9)

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

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

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:0)

我的“理论”是 CancellationToken 的作用是提供一个线程安全的包装器来避免争用

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

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

这是纯粹的推测,我可能完全错了,并且/或者这个设计可能有其他充分的理由。

唯一可以肯定的是,至少有一个很好的理由,而且这种决定没有更多的记录,因为它在使用技术时是一种非常宝贵的见解,这是一种遗憾。