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

时间:2011-06-28 21:23:30

标签: 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作为基类约束是我能做的最好的。然后我创建一个锁对象并将其锁定在公共事件的添加和删除方法中,这些方法会更改名为“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;”这一行导致编译器错误“Operator'+ ='无法应用于类型'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{}
    }
}


问题

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

7 个答案:

答案 0 :(得分:18)

答案 1 :(得分:2)

RaiseEventSafely内的锁是不必要和危险的。

这是不必要的,因为委托是不可变的。一旦你阅读它,你获得的invokation列表将不会改变。如果在事件代码运行时发生更改,或者更改需要等到之后,则无关紧要。

这是危险的,因为你在持有锁的同时调用外部代码。这很容易导致锁定顺序违规,从而导致死锁。考虑一个事件处理程序,它产生一个试图修改事件的新线程。繁荣,僵局。

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

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

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

答案 2 :(得分:1)

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

答案 3 :(得分:1)

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

您需要在课程文档中添加一条注释,表示可以在从事件中删除代理后调用代理。

我之所以这样做,是因为否则会有性能后果和可能的死锁风险。你在打电话给别人的代码时拿着锁。我们称你的内部锁为'A'。如果其中一个处理程序试图获取私有锁'B',并且在一个单独的线程上有人试图在持有锁'B'时注册一个处理程序,那么一个线程在尝试获取'B'时保持锁'A'并且在尝试获取锁定'A'时,不同的线程持有锁'B'。死锁。

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

最后一个挑剔,你认为SafeEventHandler真的描述了这门课的作用吗?它看起来像是一个事件注册商和调度员。

答案 4 :(得分:1)

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

此外,我不确定我是否遵循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 { }
            };
        }
    }

答案 5 :(得分:1)

JuvalLöwy在他的“Programming .NET components”一书中提供了这方面的实现。

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

答案 6 :(得分:-2)

我考虑了所有人都说的话,现在就得到了以下代码:

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的公共方法之一然后在该线程上等待的线程。在任何其他情况下,其他线程将直接阻塞,直到锁拥有线程释放锁。此外,虽然我根本不相信忽略错误,但我也不相信这个组件可以在任何地方智能地处理订阅者错误,也不会对这些错误的严重性做出判断调用,所以不要抛弃它们而冒险崩溃应用程序,它向调用者报告“提升”错误,因为调用者可能处于更好的位置来处理此类错误。话虽如此,这些组件为C#事件系统中缺少的事件提供了一种稳定性。

我认为人们担心的是让其他订阅者在发生错误后运行意味着他们在不稳定的环境中运行。虽然这可能是真的,但这意味着应用程序实际上是以错误的方式编写的。崩溃并不是允许代码运行的更好解决方案,因为允许代码运行将允许报告错误,并且将允许显示错误的完整效果,反过来,这将有助于工程师更多快速彻底地了解错误的本质并更快地修复它们。