【问题标题】:What are the pros and cons of using CancellationTokens as alternatives to events?使用 CancellationTokens 作为事件的替代品有什么优缺点?
【发布时间】:2021-01-22 11:17:55
【问题描述】:

最近我遇到了 a Microsoft interface 一个非常不寻常的 API:

public interface IHostApplicationLifetime
{
    public CancellationToken ApplicationStarted { get; }
    public CancellationToken ApplicationStopping { get; }
    public CancellationToken ApplicationStopped { get; }
}

ApplicationStopping 属性的文档令人困惑地暗示此属性实际上是一个事件(已添加重点):

在应用程序主机执行正常关闭时触发。在此事件完成之前,关闭将被阻止。

似乎应该是传统的EventHandler 事件,已被CancellationToken 属性所取代。这就是我所期望的 界面为:

public interface IHostApplicationLifetime
{
    public event EventHandler ApplicationStarted;
    public event EventHandler ApplicationStopping;
    public event EventHandler ApplicationStopped;
}

我的问题是,这两种通知机制是否等效?如果不是,从 API 设计者的角度来看,每种方法的优缺点是什么?在哪些情况下CancellationToken 属性优于经典事件?

【问题讨论】:

  • use an event vs a cancellation token 完全不同的概念和含义。您能否用您尝试过的内容、无效的内容和预期的输出来更新您的帖子?
  • 一个事件,我假设你的意思是 System.Threading.AutoResetEvent 或类似的,与取消令牌非常不同。
  • 可以将CancelationToken 烘焙到方法中并使其可取消。你会如何对一个事件做同样的事情?您能否通过使用事件而不是 CancelationTokenBlockingCollection<T>.Take 方法的替代示例来更新您的问题?
  • CancellationToken 超过 EventHandler 的原因很简单。使用 EventHandler,您将不知道 ApplicationStarted 是否在您的 EventHandler 注册运行之前运行。这意味着您需要添加 bool IsApplicationStarted { get; } 属性。 CancellationToken 也会隐式处理事件的生命周期。
  • 这里有一篇博文解释了这个架构决策:andrewlock.net/… 这样是否更清楚?

标签: c# events .net-core cancellation-token


【解决方案1】:

Microsoft 应该对文档更加谨慎。将 CancellationTokens 描述为要“触发”的事件是令人困惑的。

我当然不是 C# 语言专家,所以希望社区能够让我们知道我是否缺少导入点。不过,让我试一试……

所以您的问题是:它们是否等效.. 仅在它们都提供了一种注册和调用回调的方式的意义上。但由于许多其他原因,没有。

CancellationTokens 是CancellationTokenSource 的包装。它们与任务实现相关联。它们是线程安全的。它们可以在外部调用,也可以由计时器调用。您可以将多个 CancellationTokenSource 链接在一起。它们维护状态,指示它们是否已取消,您可以查询该状态。

C#events,语言特性,在文档中是一个特殊的多播委托。它们只能在声明它们的类中调用。 They are not really thread safe。它们被烘焙到 XAML 和 WPF 中。

我不会过多评论其中一个优于另一个,因为它们非常不同。在正常课程中,我认为您不会在考虑 CancellationTokens 的情况下考虑事件。它们在 IHostApplicationLifetime 的情况下重叠,主要是因为糟糕的文档和 .NET Core 托管基础设施的重新设计。正如@EricLippert 提到的,link 提供了一个很好的概述。

【讨论】:

  • +1 link!关于线程安全,在阅读了 Stephen Cleary 的文章后,我突然想到 CancellationToken 可能会遭受与事件类似的竞争条件:可以从 Thread1 处理 CancellationTokenRegistrationregistered 回调已经存在在 Thread2 上运行。
  • 但后来我注意到方法Unregister 返回一个bool。如果结果是true,那么您可以安全地处置回调所引用的任何一次性资源,因为可以保证回调既不会运行,也不会在未来运行。我认为这种反馈是消除困扰赛事的比赛条件的关键!
【解决方案2】:

CancellationToken 不等同于经典事件。差异很多,最明显的是CancellationToken 只能触发(取消)一次。因此,如果一个事件被多次提出是有意义的,那么没有两难选择:经典的event 是唯一的选择。因此,必须将比较范围缩小到一次性事件的情况,其中CancellationToken 有很多优点,而只有一个潜在的缺点。首先是优点

  1. CancellationToken 不仅会通知其订阅者未来可能发生的取消,还会通知其订阅者已经发生的取消。尝试在具有经典事件和 bool 字段的多线程应用程序中执行相同操作会创建竞争条件。在检查bool 字段和订阅事件之间可能会触发事件,在这种情况下,通知将丢失。

  2. 可以将事件处理程序附加和分离到CancellationToken,即使处理程序是匿名 lambda 函数。这在经典事件中是不可能的。 detach operator (-=) 需要与之前在订阅 (+=) 中传递的处理程序相同的处理程序,因此必须将处理程序分配给一个变量,或者是一个命名函数。

  3. 可以将CancellationToken 作为参数传递给另一个类的方法,以允许注册(和可选地取消注册)回调。这在经典事件中是不可能的。 C# 语言does not allow it。实现此功能的唯一方法是将附加/分离 lambda 作为参数传递给方法:otherClass.SomeMethod(h => this.MyEvent += h, h => this.MyEvent -= h)。这比otherClass.SomeMethod(this.MyToken) 复杂、笨拙且可读性差。

  4. 有数以千计的 API 接受 CancellationToken 参数。这使得在多种场景中使用这种通知非常方便。相反,可以通过接受addHandler/removeHandler 参数来消费事件的 API 极为罕见(例如:Observable.FromEventPattern)。

  5. 使用 Unregister¹ 方法从 CancellationToken 取消注册回调会给出 bool 反馈,无论回调是否已被调用。相反,使用 -= 操作员取消订阅经典活动不会提供任何反馈。这意味着在多线程应用程序中,调用者无法知道分离的处理程序是否已经在另一个线程上运行,因此它可以安全地处理处理程序引用的任何一次性资源。这给调用者留下了尴尬的选择,比如不释放资源,或者在处理程序中捕获可能的ObjectDisposedExceptions。

使用CancellationToken 作为一次性通知的缺点纯粹是语义上的。这种类型与取消的概念密切相关,因此使用它来通知,例如,某事开始或停止,很可能会造成混乱。

¹ 不适用于 .NET Framework,因此此优势不适用于此平台。

【讨论】:

    猜你喜欢
    • 2012-07-20
    • 2023-03-18
    • 1970-01-01
    • 2013-05-27
    • 2017-10-08
    • 2018-04-03
    • 1970-01-01
    • 1970-01-01
    • 2010-09-08
    相关资源
    最近更新 更多