繁体   English   中英

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

[英]How to implement a thread safe error-free event handler in C#?

问题背景

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

这可以通过如下代码轻松完成:

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


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

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

我决定将其实现为通用 class ,其中通用类型受“where”子句约束为委托。 我真的希望约束是“委托”或“事件”,但这些都是无效的,所以使用委托作为基本 class 约束是我能做的最好的。 然后我创建一个锁 object 并将其锁定在公共事件的添加和删除方法中,这会改变一个名为“event_handlers”的私有委托变量。

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; ”,并且编译得很好。 但我也想,既然“event”关键字可以生成代码来处理这个问题,我应该也可以,所以我最终将它更改为这个以避免编译器无法识别 '+=' 应该适用于泛型类型被限制为委托。 私有变量“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{}
    }
}


问题

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

由于任何一个处理程序都可能引发错误,这将阻止调用其中的 rest,

你说喜欢它是一件坏事。 这是一件非常好的事情 当抛出一个未处理的、意外的异常时,这意味着整个进程现在处于一个未知的、不可预测的、可能危险的不稳定 state 中。

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

我想忽略每个处理程序抛出的任何错误。

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

换句话说,我不希望一个处理程序中的错误中断调用列表中其他处理程序的执行,因为这些其他处理程序和事件发布者都无法控制任何特定事件处理程序的代码所做的事情。

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

然后我创建一个锁 object 并将其锁定在公共事件的添加和删除方法中,这会改变一个名为“event_handlers”的私有委托变量。

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

但在那种情况下,这段代码非常、非常、非常危险:

    lock (collection_lock)
        foreach (Delegate handler in event_delegate.GetInvocationList())
            try {handler.DynamicInvoke( args );}catch{}

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

线程 Alpha 进入收集锁。

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

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

线程 Beta 现在正在等待线程 Alpha。 Thread Alpha 现在调用一个委托,委托决定它要访问 foo。 所以它在线程 Beta 上等待,现在我们遇到了死锁。

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

好的,所以让我们假设您这样做:

    Delegate copy;
    lock (collection_lock)
        copy = event_delegate;
    foreach (Delegate handler in copy.GetInvocationList())
        try {handler.DynamicInvoke( args );}catch{}

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

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

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

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

Thread Alpha 再次接管并从副本中启动 X。 因为 Beta 刚刚破坏了正确执行 X 所必需的 state,所以 X 死锁。 再一次,你陷入了僵局。

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


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

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

如果它们不正确并最终引发异常,则立即停止该过程 不要一直纠结于试图运行现在处于不稳定进程中的代码。

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

RaiseEventSafely中的锁既不必要又危险。

这是不必要的,因为委托是不可变的。 阅读后,您获得的调用列表不会改变。 更改是否在事件代码运行时发生,或者更改是否需要等到之后都没有关系。

这很危险,因为您在持有锁的同时调用外部代码。 这很容易导致锁顺序冲突,从而导致死锁。 考虑一个事件处理程序,它产生一个尝试修改事件的新线程。 轰隆隆,僵局。

你有一个空的exception catch 这很少是一个好主意,因为它默默地吞下了异常。 至少您应该记录异常。

您的通用参数不以T开头。 这让 IMO 有点困惑。

where EventType:Delegate我认为这不会编译。 Delegate不是有效的通用约束。 出于某种原因,C# 规范禁止某些类型作为通用约束,其中之一是Delegate (不知道为什么)

您是否查看过PRISM EventAggregator 或MVVMLight Messenger类? 这两个课程都满足您的所有要求。 MVVMLight 的 Messenger class 使用 WeakReferences 来防止 memory 泄漏。

除了吞下异常是一个坏主意之外,我建议您在调用委托列表时考虑不要锁定。

您需要在您的班级文档中添加备注,说明在从事件中删除后可以调用委托。

我这样做的原因是因为否则你会冒性能后果和可能出现死锁的风险。 你在调用别人的代码时持有锁。 让我们将您的内部锁 Lock 称为“A”。 如果其中一个处理程序尝试获取私有锁“B”,并且在单独的线程上有人尝试在持有锁“B”的同时注册处理程序,则一个线程在尝试获取“B”时持有锁“A”和不同的线程在尝试获取锁“A”时持有锁“B”。 僵局。

像您这样的第三方库通常是在没有线程安全的情况下编写的,以避免此类问题,并且由客户端来保护访问内部变量的方法。 我认为事件 class 提供线程安全是合理的,但我认为“延迟”回调的风险比容易死锁的定义不明确的锁层次结构要好。

最后挑剔,你认为SafeEventHandler真的描述了这个 class 的作用吗? 在我看来,它就像一个事件注册器和调度器。

完全吞下异常是一种不好的做法。 如果您有一个用例,您希望发布者从订阅者引发的错误中正常恢复,那么这需要使用事件聚合器。

此外,我不确定我是否遵循 SafeEventHandler.RaiseEventSafely 中的代码。 为什么有一个事件委托作为参数? 它似乎与 event_handlers 字段没有关系。 就线程安全而言,在调用 GetInvocationList 之后,是否修改原始委托集合并不重要,因为返回的数组不会改变。

如果必须,我建议您改为执行以下操作:

class MyClass
    {
        event EventHandler myEvent;

        public event EventHandler MyEvent
        {
            add { this.myEvent += value.SwallowException(); }
            remove { this.myEvent -= value.SwallowException(); }
        }

        protected void OnMyEvent(EventArgs args)
        {
            var e = this.myEvent;
            if (e != null)
                e(this, args);
        }
    }

    public static class EventHandlerHelper
    {
        public static EventHandler SwallowException(this EventHandler handler)
        {
            return (s, args) =>
            {
                try
                {
                    handler(s, args);
                }
                catch { }
            };
        }
    }

Juval Löwy 在他的“Programming .NET components”一书中提供了一个实现。

http://books.google.com/books?id=m7E4la3JAVcC&lpg=PA129&pg=PA143#v=onepage&q&f=false

我考虑了每个人所说的一切,现在得出以下代码:

public class SafeEvent<EventDelegateType> where EventDelegateType:class
{
    private object collection_lock = new object();
    private Delegate event_handlers;

    public SafeEvent()
    {
        if(!typeof(Delegate).IsAssignableFrom( typeof(EventDelegateType) ))
            throw new ArgumentException( "Generic parameter must be a delegate type." );
    }

    public Delegate Handlers
    {
        get
        {
            lock (collection_lock)
                return (Delegate)event_handlers.Clone();
        }
    }

    public void AddEventHandler( EventDelegateType handler )
    {
        lock(collection_lock)
            event_handlers = Delegate.Combine( event_handlers, handler as Delegate );
    }

    public void RemoveEventHandler( EventDelegateType handler )
    {
        lock(collection_lock)
            event_handlers = Delegate.Remove( event_handlers, handler as Delegate );
    }

    public void Raise( object[] args, out List<Exception> errors )
    {
        lock (collection_lock)
        {
            errors = null;
            foreach (Delegate handler in event_handlers.GetInvocationList())
            {
                try {handler.DynamicInvoke( args );}
                catch (Exception err)
                {
                    if (errors == null)
                        errors = new List<Exception>();
                    errors.Add( err );
                }
            }
        }
    }
}

这绕过了编译器将 Delegate 视为无效基础 class 的特殊处理。 此外,不能将事件键入为委托。

以下是如何使用 SafeEvent 在 class 中创建事件:

private SafeEvent<SomeEventHandlerType> a_safe_event = new SafeEvent<SomeEventHandlerType>();
public event SomeEventHandlerType MyEvent
{
    add {a_safe_event.AddEventHandler( value );}
    remove {a_safe_event.RemoveEventHandler( value );}
}

以下是引发事件和处理错误的方式:

List<Exception> event_handler_errors;
a_safe_event.Raise( new object[] {event_type, disk}, out event_handler_errors );
//Report errors however you want; they should not have occurred; examine logged errors and fix your broken handlers!

总而言之,该组件的工作是以原子方式将事件发布到订阅者列表(即,在执行调用列表时不会重新引发事件并且不会更改调用列表)。 死锁是可能的,但很容易通过控制对 SafeEvent 的访问来避免,因为处理程序必须生成一个调用 SafeEvent 的公共方法之一的线程,然后在该线程上等待。 在任何其他情况下,其他线程将简单地阻塞,直到拥有锁的线程释放锁。 此外,虽然我根本不相信忽略错误,但我也不相信这个组件可以在任何地方智能地处理订阅者错误,也不会对此类错误的严重性做出判断,因此与其抛出它们并冒着崩溃的风险应用程序,它会向“Raise”的调用者报告错误,因为调用者可能在更好的 position 中来处理此类错误。 话虽如此,该组件为事件提供了一种稳定性,这是 C# 事件系统所缺乏的。

我认为人们担心的是,在发生错误后让其他订阅者运行意味着他们在不稳定的环境中运行。 虽然这可能是真的,但这意味着应用程序实际上以任何方式编写都不正确。 崩溃并没有比允许代码运行更好的解决方案,因为允许代码运行将允许报告错误,并使错误的全部影响显现出来,这反过来又将帮助工程师更多快速彻底地了解错误的性质并更快地修复他们的代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM