c# - 如何在 C# 中实现线程安全的无错误事件处理程序?

标签 c# events delegates error-handling thread-safety

问题背景

一个事件可以有多个订阅者(即在引发事件时可以调用多个处理程序)。由于任何一个处理程序都可能抛出错误,并且会阻止其余的处理程序被调用,因此我想忽略从每个单独的处理程序抛出的任何错误。换句话说,我不希望一个处理程序中的错误中断调用列表中其他处理程序的执行,因为这些其他处理程序和事件发布者都无法控制任何特定事件处理程序的代码所做的事情。

这可以使用如下代码轻松完成:

public event EventHandler MyEvent;
public void RaiseEventSafely( object sender, EventArgs e )
{
    foreach(EventHandlerType handler in MyEvent.GetInvocationList())
        try {handler( sender, e );}catch{}
}

通用的、线程安全的、无错误的解决方案

当然,我不想每次调用事件时都一遍遍地编写所有这些通用代码,所以我想将它封装在一个通用类中。此外,我实际上需要额外的代码来确保线程安全,以便在执行方法列表时 MyEvent 的调用列表不会更改。

我决定将其实现为泛型类,其中泛型类型受“where”子句约束为委托(delegate)。我真的希望约束是“委托(delegate)”或“事件”,但这些都是无效的,所以使用委托(delegate)作为基类约束是我能做的最好的事情。然后我创建一个锁定对象并将其锁定在公共(public)事件的添加和删除方法中,这会改变一个名为“event_handlers”的私有(private)委托(delegate)变量。
public class SafeEventHandler<EventType> where EventType:Delegate
{
    private object collection_lock = new object();
    private EventType event_handlers;

    public SafeEventHandler(){}

    public event EventType Handlers
    {
        add {lock(collection_lock){event_handlers += value;}}
        remove {lock(collection_lock){event_handlers -= value;}}
    }

    public void RaiseEventSafely( EventType event_delegate, object[] args )
    {
        lock (collection_lock)
            foreach (Delegate handler in event_delegate.GetInvocationList())
                try {handler.DynamicInvoke( args );}catch{}
    }
}

+= 运算符的编译器问题,但有两个简单的解决方法

遇到的一个问题是“event_handlers += value;”这一行导致编译器错误“运算符 '+=' 不能应用于类型 'EventType' 和 'EventType'”。即使 EventType 被限制为 Delegate 类型,它也不允许在其上使用 += 运算符。

作为一种解决方法,我只是将 event 关键字添加到“event_handlers”中,因此定义类似于“private event EventType event_handlers;”,并且编译正常。但我也认为,由于“事件”关键字可以生成代码来处理这个问题,我应该也可以,所以我最终将其更改为这样,以避免编译器无法识别 '+=' SHOULD 适用于泛型类型被限制为委托(delegate)。私有(private)变量“event_handlers”现在被输入为 Delegate 而不是通用的 EventType,并且添加/删除方法遵循此模式 event_handlers = MulticastDelegate.Combine( event_handlers, value );
最终代码如下所示:
public class SafeEventHandler<EventType> where EventType:Delegate
{
    private object collection_lock = new object();
    private Delegate event_handlers;

    public SafeEventHandler(){}

    public event EventType Handlers
    {
        add {lock(collection_lock){event_handlers = Delegate.Combine( event_handlers, value );}}
        remove {lock(collection_lock){event_handlers = Delegate.Remove( event_handlers, value );}}
    }

    public void RaiseEventSafely( EventType event_delegate, object[] args )
    {
        lock (collection_lock)
            foreach (Delegate handler in event_delegate.GetInvocationList())
                try {handler.DynamicInvoke( args );}catch{}
    }
}

问题

我的问题是......这似乎能很好地完成这项工作吗?有没有更好的方法,或者这基本上是必须完成的方式?我想我已经用尽了所有的选择。在公共(public)事件的添加/删除方法中使用锁(由私有(private)委托(delegate)支持)并在执行调用列表时使用相同的锁是我能看到的使调用列表线程安全的唯一方法,同时还确保处理程序抛出的错误不会干扰其他处理程序的调用。

最佳答案

Since any one of the handlers could throw an error, and that would prevent the rest of them from being called,



你说那是坏事。 这是一件非常好的事情 .当一个未处理的、意外的异常被抛出时,这意味着整个过程现在处于一种未知的、不可预测的、可能是危险的不稳定状态。

此时运行更多代码可能会使事情变得更糟,而不是更好。发生这种情况时,最安全的做法是检测情况并导致故障快速,从而在不运行任何代码的情况下关闭整个进程。您不知道此时运行更多代码会发生什么可怕的事情。

I want to ignore any errors thrown from each individual handler.



这是一个 super 危险的想法。这些异常告诉你一些可怕的事情正在发生,而你却忽略了它们。

In other words, I do not want an error in one handler to disrupt the execution of other handlers in the invocation list, since neither those other handlers nor the event publisher has any control over what any particular event handler's code does.



这里谁负责?有人将这些事件处理程序添加到此事件中。这是负责确保事件处理程序在出现异常情况时做正确事情的代码。

I then create a lock object and lock it in a public event's add and remove methods, which alter a private delegate variable called "event_handlers".



当然,那很好。我质疑该功能的必要性——我很少遇到多个线程向一个事件添加事件处理程序的情况——但我相信你会遇到这种情况。

但在那种情况下,这段代码非常、非常、非常危险:
    lock (collection_lock)
        foreach (Delegate handler in event_delegate.GetInvocationList())
            try {handler.DynamicInvoke( args );}catch{}

让我们想想那里出了什么问题。

线程 Alpha 进入集合锁。

假设还有另一个资源 foo,它也由不同的锁控制。线程 Beta 进入 foo 锁以获得它需要的一些数据。

线程 Beta 然后获取该数据并尝试进入集合锁,因为它想在事件处理程序中使用 foo 的内容。

线程 Beta 现在正在等待线程 Alpha。线程 Alpha 现在调用一个委托(delegate),它决定要访问 foo。所以它在线程 Beta 上等待,现在我们有一个死锁。

但是我们不能通过订购锁来避免这种情况吗? 不,因为您的场景的前提是您不知道事件处理程序在做什么! 如果您已经知道事件处理程序在锁定顺序方面表现良好,那么您大概也知道它们在不抛出异常方面表现良好,整个问题就消失了。

好的,让我们假设您这样做:
    Delegate copy;
    lock (collection_lock)
        copy = event_delegate;
    foreach (Delegate handler in copy.GetInvocationList())
        try {handler.DynamicInvoke( args );}catch{}

委托(delegate)是不可变的并且通过引用原子地复制,所以您现在知道您将调用 event_delegate 的内容,但在调用期间不持有锁。这有帮助吗?

并不真地。你用一个问题换了另一个问题:

线程 Alpha 获取锁并复制委托(delegate)列表,然后离开锁。

线程 Beta 获取锁,从列表中删除事件处理程序 X,并销毁防止 X 死锁所需的状态。

线程 Alpha 再次接管并从副本中启动 X。因为 Beta 只是破坏了正确执行 X 所必需的状态,所以 X 会发生死锁。再一次,你陷入僵局。

事件处理程序必须不这样做;面对突然变得“陈旧”的情况,他们必须保持坚强。听起来您处于无法相信事件处理程序编写良好的场景中。这是一个可怕的情况;那么你就不能相信任何代码在这个过程中是可靠的。您似乎认为可以通过捕获所有错误并混过去来对事件处理程序施加某种程度的隔离,但事实并非如此。事件处理程序只是代码,它们可以像任何其他代码一样影响程序中的任意全局状态。

简而言之,您的解决方案是通用的,但它不是线程安全的,也不是没有错误的。相反,它会加剧死锁等线程问题,并关闭安全系统。

您根本无法放弃确保事件处理程序正确的责任,所以不要尝试。 编写您的事件处理程序,使它们正确无误——以便它们正确地对锁进行排序并且永远不会抛出未处理的异常。

如果它们不正确并最终抛出异常,则 立即关闭进程 .不要一头雾水地尝试运行现在处于不稳定进程中的代码。

根据您对其他答案的评论,您似乎认为您应该能够从陌生人那里拿糖果而不会产生不良影响。你不能,不是没有更多的隔离。您不能只是随意地为过程中的事件注册随机代码并希望最好。如果您有一些不可靠的东西,因为您在应用程序中运行第三方代码,则需要某种托管加载项框架来提供隔离。尝试查找 MEF 或 MAF。

关于c# - 如何在 C# 中实现线程安全的无错误事件处理程序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6513249/

相关文章:

C# 委托(delegate)教程工作

c# - 找不到属性设置方法

通过ajax的Jquery asp.net按钮点击事件

javascript - 。原型(prototype)。不适用于事件监听器

angular - 如何在 Angular 中跟踪所有应用程序动画结束的时刻?

ios - Objective-C : get location without delegate

c# - 此 C 函数的正确 C# PInvoke 签名

c# - 是否可以将单击事件用于WPF(MVVM)中的小型操作?

c# - Linq to sql - 左外连接

ios - 设置后委托(delegate)nil