ChatGPT解决这个技术问题 Extra ChatGPT

为什么 CancellationToken 与 CancellationTokenSource 是分开的?

我正在寻找为什么除了 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,并且对于每个令牌,取消检查都是本地字段访问?

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

谢谢!


P
Pang

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

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

如果对行为不端的 API 敞开大门,那么取消设计的实用性可能会很快被削弱。

CancellationTokenSource == "取消触发器",加上生成链接的侦听器

CancellationToken == "取消监听器"


非常感谢!内部知识真的很感激。
stackoverflow.com/questions/39077497/… 您知道 StreamSocket 的输入流的默认超时时间是多少吗?当我使用 canceltoken 取消读取操作时,它也会关闭相关的套接字。有什么办法可以克服这个问题吗?
@Mike——只是好奇:为什么你不能像在 CT 上那样在 CTS 上调用 ThrowIfCancellationRequested() 之类的东西?
他们有不同的职责,编写 cts.Token.ThrowIfCancellationRequested() 并不太冗长,因此我们没有直接在 CTS 上添加侦听器 API。
CancellationTriggerCancellationListener 是更好的名称。我想知道是否有办法改变这样的事情。
S
SolutionYogi

我有一个确切的问题,我想了解这个设计背后的基本原理。

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

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

链接:.NET 4 Cancellation Framework

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

我也觉得它使 API 更干净,避免意外错误,促进更好的组件设计。

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

https://i.stack.imgur.com/J8xDX.png

https://i.stack.imgur.com/S3tsN.png

如果你将它们结合起来,在编写 LongRunningFunction 时,我会看到那些我不应该使用的方法,比如“取消”的多个重载。就个人而言,我也讨厌看到 Dispose 方法。

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

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

我相信 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) 的引用
为什么要在应用程序中包含“敌对”第 3 方代码?
C
Cory Nelson

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

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


谢谢!我的眨眼反应是,从语义上讲,这是一种有问题的方法,与提供 IReadOnlyList 相当。不过,这听起来确实很合理,所以接受你的回答。
经过进一步思考,我发现自己质疑这种合理性。那么,提供 CancellationTokenSource 将实现的 ICancellationToken 接口不是更有意义吗?我希望 .NET 团队的某个人能加入进来。
这可能仍会导致狡猾的程序员强制转换为 CancellationTokenSource。你会认为你可以说“不要那样做”,但是人们(包括我!)偶尔会做这些事情来获得一些隐藏的功能,而且它会发生。这是我目前的理论,至少。
在大多数情况下,这不是您设计 API 的方式,除非它与安全相关。我宁愿打赌其他一些解释,比如“为未来的性能优化保持开放的选择”。但是,您的仍然是迄今为止最好的。
嘿,我希望你能原谅我将接受的答案重新分配给参与实施的人。虽然你们都说同样的话,但我认为 SO 受益于明确的答案。
E
Eliahu Aaron

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

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

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


谢谢。请注意,另一个答案告诉技术上(在 IL 中),CancellationTokenSource 不会设置令牌的状态;相反,令牌包装了对 CancellationTokenSource 的实际引用,并简单地访问它以检查取消。如果有的话,速度只能在这里丢失。
+1。我不明白。如果它是一个值类型,那么每个方法都有自己的值(单独的值)。那么方法如何知道是否已调用取消?它没有对 TokenSource 的任何引用。所有方法可以看到的是一个局部值类型,如“5”。你可以解释吗 ?
@RoyiNamir:每个 CancellationToken 都有 CancellationTokenSource 类型的私有引用“m_source”
E
Eliahu Aaron

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

这非常适合单元测试顺便说一句 - 创建您自己的取消令牌源,获取令牌,在源上调用 Cancel,然后将令牌传递给您必须处理取消的代码。


谢谢——但是这两个职责(非常相关)可以交给同一个班级。并没有真正解释为什么它们是分开的。