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

发布于 2024-11-17 23:41:25 字数 2876 浏览 2 评论 0原文

问题背景

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

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

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


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

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

我决定将其实现为一个泛型类,其中泛型类型受“where”子句限制为委托。我真的希望约束是“委托”或“事件”,但这些都是无效的,所以使用委托作为基类约束是我能做的最好的事情。然后,我创建一个锁对象并将其锁定在公共事件的添加和删除方法中,这会更改名为“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 被限制为委托类型,它也不允许使用 += 运算符。

作为解决方法,我只是将 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{}
    }
}


问题

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

Problem background

An event can have multiple subscribers (i.e. 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. 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. 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.

I decided to implement this as a generic class where the generic type is constrained by the "where" clause to be a Delegate. 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. 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".

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;" results in the compiler error "Operator '+=' cannot be applied to types 'EventType' and 'EventType'". Even though EventType is constrained to be a Delegate type, it will not allow the += operator on it.

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

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

眼前雾蒙蒙 2024-11-24 23:41:25

由于任何一个处理程序都可能抛出错误,这将阻止其余的处理程序被调用,

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

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

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

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

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

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

然后,我创建一个锁对象并将其锁定在公共事件的添加和删除方法中,这会更改名为“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 现在调用一个委托,该委托决定它想要访问 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 死锁所需的状态。

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

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


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

您根本不能放弃确保事件处理程序正确的责任,所以不要尝试。编写您的事件处理程序,使其正确 - 以便它们正确排序锁并且永远不会抛出未处理的异常。

如果它们不正确并最终引发异常,则立即停止该流程。不要继续尝试运行现在处于不稳定进程中的代码。

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

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

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.

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".

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.

Suppose there is another resource, foo, which is also controlled by a different lock. Thread Beta enters the foo lock in order to obtain some data that it needs.

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 is now waiting on thread Alpha. Thread Alpha now calls a delegate, which decides that it wants to access foo. So it waits on thread Beta, and now we have a deadlock.

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. 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.

Thread Beta takes the lock, removes event handler X from the list, and destroys state necessary to prevent X from deadlocking.

Thread Alpha takes over again and starts up X from the copy. Because Beta just destroyed state necessary for the correct execution of X, X deadlocks. 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.


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.

一个人的夜不怕黑 2024-11-24 23:41:25

RaiseEventSafely 内部的锁既不必要又危险。

这是不必要的,因为代表是不可变的。阅读后,您获得的调用列表不会改变。更改是在事件代码运行时发生还是需要等到事件代码运行之后才发生并不重要。

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

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

您的通用参数不以 T 开头。在我看来,这有点令人困惑。

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

The lock inside RaiseEventSafely is both unnecessary and dangerous.

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. 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. That's a bit confusing IMO.

where EventType:Delegate I don't think this compiles. Delegate is not a valid generic constraint. For some reason the C# specification forbids certain types as a generic constraint, and one of them is Delegate. (no idea why)

一江春梦 2024-11-24 23:41:25

您是否研究过 PRISM EventAggregator 或 MVVMLight Messenger 类?这两个课程都满足您的所有要求。 MVVMLight 的 Messenger 类使用 Wea​​kReferences 来防止内存泄漏。

Have you looked into the PRISM EventAggregator or MVVMLight Messenger classes? Both of these classes fulfill all your requirements. MVVMLight's Messenger class uses WeakReferences to prevent memory leaks.

晚雾 2024-11-24 23:41:25

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

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

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

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

最后,您认为 SafeEventHandler 真的描述了这个类的作用吗?对我来说,它看起来像是一个事件注册器和调度器。

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'. 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'. 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.

Last nit-pick, do you think SafeEventHandler really describes what this class does? It looks like an event registrar and dispatcher to me.

何以畏孤独 2024-11-24 23:41:25

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

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

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

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

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. Why is there an event delegate as a parameter? It seems to have no relationship with the event_handlers field. 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.

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 { }
            };
        }
    }
将军与妓 2024-11-24 23:41:25

Juval Löwy 在他的《Programming .NET Components》一书中提供了一个实现。

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

Juval Löwy provides an implementation of this in his book "Programming .NET components".

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

淡淡離愁欲言轉身 2024-11-24 23:41:25

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

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 作为无效基类的特殊处理。此外,事件不能键入为“委托”。

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

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”的调用者报告错误,因为调用者可能能够更好地处理此类错误。尽管如此,该组件为事件提供了一种 C# 事件系统所缺乏的稳定性。

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

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. Also, events cannot be typed as Delegate.

Here is how a SafeEvent would be used to create an event in a 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 (i.e. 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. 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. With that said, this components provides a kind of stability to events that's lacking in the C# event system.

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文