简体   繁体   English

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

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

Problem background问题背景

An event can have multiple subscribers (ie multiple handlers may be called when an event is raised).一个事件可以有多个订阅者(即,在引发事件时可以调用多个处理程序)。 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.由于任何一个处理程序都可能引发错误,这将阻止调用它们中的 rest,因此我想忽略每个单独的处理程序引发的任何错误。 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.换句话说,我不希望一个处理程序中的错误中断调用列表中其他处理程序的执行,因为这些其他处理程序和事件发布者都无法控制任何特定事件处理程序的代码所做的事情。

This can be accomplished easily with code like this:这可以通过如下代码轻松完成:

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


A generic, thread-safe, error-free solution通用、线程安全、无错误的解决方案

Of course, I don't want to write all this generic code over and over every time I call an event, so I wanted to encapsulate it in a generic class.当然,我不想每次调用事件时都一遍又一遍地编写所有这些通用代码,所以我想将它封装在一个通用的 class 中。 Furthermore, I'd actually need additional code to ensure thread-safety so that MyEvent's invocation list does not change while the list of methods is being executed.此外,我实际上需要额外的代码来确保线程安全,以便在执行方法列表时 MyEvent 的调用列表不会改变。

I decided to implement this as a generic class where the generic type is constrained by the "where" clause to be a Delegate.我决定将其实现为通用 class ,其中通用类型受“where”子句约束为委托。 I really wanted the constraint to be "delegate" or "event", but those are not valid, so using Delegate as a base class constraint is the best I can do.我真的希望约束是“委托”或“事件”,但这些都是无效的,所以使用委托作为基本 class 约束是我能做的最好的。 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".然后我创建一个锁 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{}
    }
}


Compiler issue with += operator, but two easy workarounds += 运算符的编译器问题,但有两个简单的解决方法

One problem ran into is that the line "event_handlers += value;"遇到的一个问题是“event_handlers += value;”这一行。 results in the compiler error "Operator '+=' cannot be applied to types 'EventType' and 'EventType'".导致编译器错误“运算符'+ ='不能应用于类型'EventType'和'EventType'”。 Even though EventType is constrained to be a Delegate type, it will not allow the += operator on it.即使 EventType 被限制为 Delegate 类型,它也不允许在其上使用 += 运算符。

As a workaround, I just added the event keyword to "event_handlers", so the definition looks like this " private event EventType event_handlers; ", and that compiles fine.作为一种解决方法,我只是将 event 关键字添加到“event_handlers”,因此定义看起来像这样“ private event EventType event_handlers; ”,并且编译得很好。 But I also figured that since the "event" keyword can generate code to handle this, that I should be able to as well, so I eventually changed it to this to avoid the compiler's inability to recognize that '+=' SHOULD apply to a generic type constrained to be a Delegate.但我也想,既然“event”关键字可以生成代码来处理这个问题,我应该也可以,所以我最终将它更改为这个以避免编译器无法识别 '+=' 应该适用于泛型类型被限制为委托。 The private variable "event_handlers" is now typed as Delegate instead of the generic EventType, and the add/remove methods follow this pattern event_handlers = MulticastDelegate.Combine( event_handlers, value );私有变量“event_handlers”现在被键入为 Delegate 而不是通用 EventType,并且添加/删除方法遵循此模式event_handlers = MulticastDelegate.Combine( event_handlers, value );


The final code looks like this:最终代码如下所示:

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{}
    }
}


The Question问题

My question is... does this appear to do the job well?我的问题是……这看起来能很好地完成这项工作吗? Is there a better way or is this basically the way it must be done?有没有更好的方法,或者这基本上是必须做的? I think I've exhausted all the options.我想我已经用尽了所有的选择。 Using a lock in the add/remove methods of a public event (backed by a private delegate) and also using the same lock while executing the invocation list is the only way I can see to make the invocation list thread-safe, while also ensuring errors thrown by handlers don't interfere with the invocation of other handlers.在公共事件的添加/删除方法中使用锁(由私有委托支持)并在执行调用列表时使用相同的锁是我可以看到使调用列表线程安全的唯一方法,同时还确保处理程序抛出的错误不会干扰其他处理程序的调用。

Since any one of the handlers could throw an error, and that would prevent the rest of them from being called,由于任何一个处理程序都可能引发错误,这将阻止调用其中的 rest,

You say that like it is a bad thing.你说喜欢它是一件坏事。 That is a very good thing .这是一件非常好的事情 When an unhandled, unexpected exception is thrown that means that the entire process is now in an unknown, unpredictable, possibly dangerously unstable state.当抛出一个未处理的、意外的异常时,这意味着整个进程现在处于一个未知的、不可预测的、可能危险的不稳定 state 中。

Running more code at this point is likely to make things worse, not better.此时运行更多代码可能会使事情变得更糟,而不是更好。 The safest thing to do when this happens is to detect the situation and cause a failfast that takes down the entire process without running any more code.发生这种情况时最安全的做法是检测情况并导致故障恢复,从而在不运行任何代码的情况下终止整个进程。 You don't know what awful thing running more code is going to do at this point.你不知道此时运行更多代码会发生什么可怕的事情。

I want to ignore any errors thrown from each individual handler.我想忽略每个处理程序抛出的任何错误。

This is a super dangerous idea.这是一个超级危险的想法。 Those exceptions are telling you that something awful is happening, and you're ignoring them.这些异常告诉您正在发生可怕的事情,而您忽略了它们。

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

Whose in charge here?这里谁负责? Someone is adding those event handlers to this event.有人正在将这些事件处理程序添加到此事件中。 That is the code that is responsible for ensuring that the event handlers do the right thing should there be an exceptional situation.是负责确保事件处理程序在出现异常情况时执行正确操作的代码。

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

Sure, that's fine.当然,那很好。 I question the necessity of the feature -- I very rarely have a situation where multiple threads are adding event handlers to an event -- but I'll take your word for it that you are in this situation.我质疑该功能的必要性——我很少遇到多个线程向一个事件添加事件处理程序的情况——但我会相信你的话,你在这种情况下。

But in that scenario this code is very, very, very dangerous:但在那种情况下,这段代码非常、非常、非常危险:

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

Let's think about what goes wrong there.让我们想想那里出了什么问题。

Thread Alpha enters the collection lock.线程 Alpha 进入收集锁。

Suppose there is another resource, foo, which is also controlled by a different lock.假设还有另一个资源 foo,它也由不同的锁控制。 Thread Beta enters the foo lock in order to obtain some data that it needs. Thread Beta 进入 foo 锁以获取它需要的一些数据。

Thread Beta then takes that data and attempts to enter the collection lock, because it wants to use the contents of foo in an event handler.然后 Thread Beta 获取该数据并尝试进入集合锁,因为它想在事件处理程序中使用 foo 的内容。

Thread Beta is now waiting on thread Alpha.线程 Beta 现在正在等待线程 Alpha。 Thread Alpha now calls a delegate, which decides that it wants to access foo. Thread Alpha 现在调用一个委托,委托决定它要访问 foo。 So it waits on thread Beta, and now we have a deadlock.所以它在线程 Beta 上等待,现在我们遇到了死锁。

But can't we avoid this by ordering the locks?但是我们不能通过订购锁来避免这种情况吗? No, because the very premise of your scenario is that you don't know what the event handlers are doing!不,因为您的场景的前提是您不知道事件处理程序在做什么! If you already know that the event handlers are well-behaved with respect to their lock ordering then you can presumably also know that they are well-behaved with respect to not throwing exceptions, and the whole problem vanishes.如果您已经知道事件处理程序在锁定顺序方面表现良好,那么您大概也知道它们在不抛出异常方面表现良好,整个问题就消失了。

OK, so let's suppose that you do this instead:好的,所以让我们假设您这样做:

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

Delegates are immutable and copied atomically by reference, so you now know that you're going to be invoking the contents of event_delegate but not holding the lock during the invocation.委托是不可变的,并且通过引用以原子方式复制,因此您现在知道您将调用 event_delegate 的内容,但在调用期间不持有锁。 Does that help?这有帮助吗?

Not really.并不真地。 You've traded one problem for another one:你已经用一个问题换了另一个问题:

Thread Alpha takes the lock and makes a copy of the delegate list, and leaves the lock.线程 Alpha 获取锁并复制委托列表,然后离开锁。

Thread Beta takes the lock, removes event handler X from the list, and destroys state necessary to prevent X from deadlocking.线程 Beta 获取锁,从列表中删除事件处理程序 X,并销毁防止 X 死锁所必需的 state。

Thread Alpha takes over again and starts up X from the copy. Thread Alpha 再次接管并从副本中启动 X。 Because Beta just destroyed state necessary for the correct execution of X, X deadlocks.因为 Beta 刚刚破坏了正确执行 X 所必需的 state,所以 X 死锁。 And once more, you are deadlocked.再一次,你陷入了僵局。

Event handlers are required to not do that;事件处理程序必须不这样做; they are required to be robust in the face of their suddenly becoming "stale".面对突然变得“陈旧”的情况,他们必须保持稳健。 It sounds like you are in a scenario where you cannot trust your event handlers to be well-written.听起来您处于无法相信事件处理程序编写良好的情况。 That's a horrid situation to be in;这是一个可怕的情况。 you then cannot trust any code to be reliable in the process.那么你就不能相信任何代码在这个过程中是可靠的。 You seem to think that there is some level of isolation you can impose on an event handler by catching all its errors and muddling through, but there is not.您似乎认为可以通过捕获事件处理程序的所有错误并蒙混过关来对事件处理程序施加某种程度的隔离,但事实并非如此。 Event handlers are just code, and they can affect arbitrary global state in the program like any other code.事件处理程序只是代码,它们可以像任何其他代码一样影响程序中的任意全局 state。


In short, your solution is generic, but it is not threadsafe and it is not error-free.简而言之,您的解决方案是通用的,但它不是线程安全的,也不是没有错误的。 Rather, it exacerbates threading problems like deadlocks and it turns off safety systems.相反,它会加剧死锁等线程问题,并关闭安全系统。

You simply cannot abdicate responsibility for ensuring that event handlers are correct, so don't try.您根本不能放弃确保事件处理程序正确的责任,所以不要尝试。 Write your event handlers so that they are correct -- so that they order locks correctly and never throw unhandled exceptions.编写您的事件处理程序,使它们是正确的——这样它们就可以正确地对锁进行排序,并且永远不会抛出未处理的异常。

If they are not correct and end up throwing exceptions then take down the process immediately .如果它们不正确并最终引发异常,则立即停止该过程 Don't keep muddling through trying to run code that is now living in an unstable process.不要一直纠结于试图运行现在处于不稳定进程中的代码。

Based on your comments on other answers it looks like you think that you should be able to take candy from strangers with no ill effects.根据您对其他答案的评论,您似乎认为您应该能够从陌生人那里拿走糖果而不会产生不良影响。 You cannot, not without a whole lot more isolation.你不能,不能没有更多的孤立。 You can't just sign up random code willy-nilly to events in your process and hope for the best.您不能随便注册随机代码到您的流程中的事件并希望最好。 If you have stuff that is unreliable because you're running third party code in your application, you need a managed add-in framework of some sort to provide isolation.如果您有一些不可靠的东西,因为您在应用程序中运行第三方代码,您需要某种托管加载项框架来提供隔离。 Try looking up MEF or MAF.尝试查找 MEF 或 MAF。

The lock inside RaiseEventSafely is both unnecessary and dangerous. RaiseEventSafely中的锁既不必要又危险。

It is unnecessary because delegates are immutable.这是不必要的,因为委托是不可变的。 Once you read it, the invokation list you obtained will not change.阅读后,您获得的调用列表不会改变。 It doesn't matter if the changes happen while event code runs, or if the changes need to wait until after.更改是否在事件代码运行时发生,或者更改是否需要等到之后都没有关系。

It is dangerous because you're calling external code while holding a lock.这很危险,因为您在持有锁的同时调用外部代码。 This can easily lead to lock order violations and thus deadlocks.这很容易导致锁顺序冲突,从而导致死锁。 Consider an eventhandler that spawns a new thread that tries to modify the event.考虑一个事件处理程序,它产生一个尝试修改事件的新线程。 Boom, deadlock.轰隆隆,僵局。

The you have an empty catch for exception .你有一个空的exception catch That's rarely a good idea, since it silently swallows the exception.这很少是一个好主意,因为它默默地吞下了异常。 At minimum you should log the exception.至少您应该记录异常。

Your generic parameter doesn't start with a T .您的通用参数不以T开头。 That's a bit confusing IMO.这让 IMO 有点困惑。

where EventType:Delegate I don't think this compiles. where EventType:Delegate我认为这不会编译。 Delegate is not a valid generic constraint. Delegate不是有效的通用约束。 For some reason the C# specification forbids certain types as a generic constraint, and one of them is Delegate .出于某种原因,C# 规范禁止某些类型作为通用约束,其中之一是Delegate (no idea why) (不知道为什么)

Have you looked into the PRISM EventAggregator or MVVMLight Messenger classes?您是否查看过PRISM EventAggregator 或MVVMLight Messenger类? Both of these classes fulfill all your requirements.这两个课程都满足您的所有要求。 MVVMLight's Messenger class uses WeakReferences to prevent memory leaks. MVVMLight 的 Messenger class 使用 WeakReferences 来防止 memory 泄漏。

Aside from it being a bad idea to swallow exceptions, I suggest you consider not locking while invoking the list of delegates.除了吞下异常是一个坏主意之外,我建议您在调用委托列表时考虑不要锁定。

You'll need to put a remark in your class's documentation that delegates can be called after having been removed from the event.您需要在您的班级文档中添加备注,说明在从事件中删除后可以调用委托。

The reason I'd do this is because otherwise you risk performance consequences and possibly deadlocks.我这样做的原因是因为否则你会冒性能后果和可能出现死锁的风险。 You're holding a lock while calling into someone else's code.你在调用别人的代码时持有锁。 Let's call your internal lock Lock 'A'.让我们将您的内部锁 Lock 称为“A”。 If one of the handlers attempts to acquire a private lock 'B', and on a separate thread someone tries to register a handler while holding lock 'B', then one thread holds lock 'A' while trying to acquire 'B' and a different thread holds lock 'B' while trying to acquire lock 'A'.如果其中一个处理程序尝试获取私有锁“B”,并且在单独的线程上有人尝试在持有锁“B”的同时注册处理程序,则一个线程在尝试获取“B”时持有锁“A”和不同的线程在尝试获取锁“A”时持有锁“B”。 Deadlock.僵局。

Third-party libraries like yours are often written with no thread safety to avoid these kinds of issues, and it is up to the clients to protect methods that access internal variables.像您这样的第三方库通常是在没有线程安全的情况下编写的,以避免此类问题,并且由客户端来保护访问内部变量的方法。 I think it is reasonable for an event class to provide thread safety, but I think the risk of a 'late' callback is better than a poorly-defined lock hierarchy prone to deadlocking.我认为事件 class 提供线程安全是合理的,但我认为“延迟”回调的风险比容易死锁的定义不明确的锁层次结构要好。

Last nit-pick, do you think SafeEventHandler really describes what this class does?最后挑剔,你认为SafeEventHandler真的描述了这个 class 的作用吗? It looks like an event registrar and dispatcher to me.在我看来,它就像一个事件注册器和调度器。

It is a bad practice to swallow exceptions entirely.完全吞下异常是一种不好的做法。 If you have a use case where you would like a publisher to recover gracefully from an error raised by a subscriber then this calls for the use of an event aggregator.如果您有一个用例,您希望发布者从订阅者引发的错误中正常恢复,那么这需要使用事件聚合器。

Moreover, I'm not sure I follow the code in SafeEventHandler.RaiseEventSafely.此外,我不确定我是否遵循 SafeEventHandler.RaiseEventSafely 中的代码。 Why is there an event delegate as a parameter?为什么有一个事件委托作为参数? It seems to have no relationship with the event_handlers field.它似乎与 event_handlers 字段没有关系。 As far as thread-safety, after the call to GetInvocationList, it does not matter if the original collection of delegates is modified because the array returned won't change.就线程安全而言,在调用 GetInvocationList 之后,是否修改原始委托集合并不重要,因为返回的数组不会改变。

If you must, I would suggest doing the following instead:如果必须,我建议您改为执行以下操作:

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 provides an implementation of this in his book "Programming .NET components". Juval Löwy 在他的“Programming .NET components”一书中提供了一个实现。

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

I considered everything everyone said, and arrived at the following code for now:我考虑了每个人所说的一切,现在得出以下代码:

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 );
                }
            }
        }
    }
}

This bypasses the compiler's special treatment of the Delegate as an invalid base class.这绕过了编译器将 Delegate 视为无效基础 class 的特殊处理。 Also, events cannot be typed as Delegate.此外,不能将事件键入为委托。

Here is how a SafeEvent would be used to create an event in a 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 );}
}

And here is how the event would be raised and errors handled:以下是引发事件和处理错误的方式:

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!

To summarize, this component's job is to publish events to a list of subscribers in an atomic manner (ie the event will not be re-raised and the invocation list will not be changed while the invocation list is executing).总而言之,该组件的工作是以原子方式将事件发布到订阅者列表(即,在执行调用列表时不会重新引发事件并且不会更改调用列表)。 Deadlock is possible but easily avoided by controlling access to the SafeEvent, because a handler would have to spawn a thread that calls one of the public methods of the SafeEvent and then wait on that thread.死锁是可能的,但很容易通过控制对 SafeEvent 的访问来避免,因为处理程序必须生成一个调用 SafeEvent 的公共方法之一的线程,然后在该线程上等待。 In any other scenario, other threads would simply block until the lock owning-thread releases the lock.在任何其他情况下,其他线程将简单地阻塞,直到拥有锁的线程释放锁。 Also, while I do not believe in ignoring errors at all, I also do not believe that this component is in any place to handle subscriber errors intelligently nor make a judgement call about the severity of such errors, so rather than throw them and risk crashing the application, it reports errors to the caller of "Raise", since the caller is likely to be in a better position to handle such errors.此外,虽然我根本不相信忽略错误,但我也不相信这个组件可以在任何地方智能地处理订阅者错误,也不会对此类错误的严重性做出判断,因此与其抛出它们并冒着崩溃的风险应用程序,它会向“Raise”的调用者报告错误,因为调用者可能在更好的 position 中来处理此类错误。 With that said, this components provides a kind of stability to events that's lacking in the C# event system.话虽如此,该组件为事件提供了一种稳定性,这是 C# 事件系统所缺乏的。

I think what people are worried about is that letting other subscribers run after an error has occurred means they are running in an unstable context.我认为人们担心的是,在发生错误后让其他订阅者运行意味着他们在不稳定的环境中运行。 While that might be true, that means the application is in fact written incorrectly any way you look at it.虽然这可能是真的,但这意味着应用程序实际上以任何方式编写都不正确。 Crashing is no better a solution than allowing the code to run, because allowing the code to run will allow errors to be reported, and will allow the full effects of the error to be manifest, and this, in turn, will assist engineers to more quickly and thoroughly understand the nature of the error and FIX THEIR CODE FASTER.崩溃并没有比允许代码运行更好的解决方案,因为允许代码运行将允许报告错误,并使错误的全部影响显现出来,这反过来又将帮助工程师更多快速彻底地了解错误的性质并更快地修复他们的代码。

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

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