问题描述
最近,我遇到了一个非常不寻常的API,a Microsoft interface:
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
属性优于经典事件?
解决方法
Microsoft应该更仔细地阅读文档。将CancellationTokens描述为要“触发”的事件令人困惑。
我当然不是C#语言专家,所以希望社区能够让我们知道我是否缺少导入点。但是让我刺几件事...
所以您的问题:它们是否等效 ..仅在它们都提供注册和调用回调的方式的意义上。但是由于许多其他原因,不。
CancellationToken围绕CancellationTokenSource进行包装。它们与Task实现紧密结合。它们是线程安全的。它们可以从外部调用,也可以由计时器调用。您可以将多个CancellationTokenSource链接在一起。它们保持状态,指示它们是否被取消,您可以查询该状态。
语言功能C#events在文档中指的是特殊的多播委托。它们只能在声明它们的类中调用。 They are not really thread safe。它们被烘焙到XAML和WPF中。
我不会在一个比另一个优越的地方发表过多评论,因为它们有很大的不同。在正常情况下,我不认为您会考虑使用CancellationTokens的情况下的事件。在IHostApplicationLifetime的情况下,它们有重叠之处,主要是因为文档不正确以及.NET Core托管基础结构的重新设计。正如@EricLippert提到的,link对此提供了很好的概述。
, CancellationToken
与经典事件不同。差异很多,最明显的是CancellationToken
只能被触发(取消)一次。因此,如果一次事件被多次提出是有意义的,则没有两难选择:经典的event是唯一的选择。因此,必须将比较范围缩小到一次性事件的情况,其中CancellationToken
具有很多优点,而只有一个潜在的缺点。首先,优势:
-
CancellationToken
不仅通知其订户将来可能发生的取消,而且还通知已经发生的取消。在具有经典事件和bool
字段的多线程应用程序中尝试执行相同操作会创建竞争条件。在检查bool
字段与订阅该事件之间可能会触发该事件,在这种情况下,通知将丢失。 -
即使事件处理程序是匿名lambda函数,也可以将事件处理程序附加到
CancellationToken
上和将其分离。对于经典事件,这是不可能的。 detach operator(-=
)需要与订阅(+=
)先前传递的处理程序相同,因此必须将处理程序分配给变量或命名函数。> -
可以将
CancellationToken
作为参数传递给另一个类的方法,以允许注册(也可以取消注册)回调。对于经典事件,这是不可能的。 C#语言does not allow it。实现此功能的唯一方法是将附加/分离lambda作为参数传递给方法otherClass.SomeMethod(h => this.MyEvent += h,h => this.MyEvent -= h)
。这比otherClass.SomeMethod(this.MyToken)
复杂,笨拙且可读性较差。 -
有成千上万个接受
CancellationToken
参数的API。这使得在多种情况下使用此类通知非常方便。相反,可以通过接受addHandler
/removeHandler
参数消耗事件的API极为罕见(例如:Observable.FromEventPattern
)。 -
使用方法
Unregister
¹从CancellationToken
注销回调,无论是否已调用该回调,都会提供bool
反馈。相反,使用-=
运算符取消订阅经典事件不会得到任何反馈。这意味着在多线程应用程序中,调用者无法知道分离的处理程序是否已在另一个线程上运行,因此它可以安全地处置该处理程序引用的所有可处理资源。这就给调用者留下了尴尬的选择,例如不处理资源或在处理程序内部捕获可能的ObjectDisposedException
。
使用CancellationToken
作为一次性通知的缺点纯粹是语义上的。这种类型与取消的概念紧密相关,因此使用它来通知例如开始或停止的某件事很有可能引起混乱。
¹不适用于.NET Framework,因此此优势不适用于此平台。