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

Tri*_*nko 6 c# error-handling events delegates thread-safety

问题背景

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

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

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


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

当然,我不想在每次调用事件时反复编写所有这些通用代码,所以我想将它封装在泛型类中.此外,我实际上需要额外的代码来确保线程安全,以便在执行方法列表时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{}
    }
}
Run Code Online (Sandbox Code Playgroud)


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

遇到的一个问题是"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{}
    }
}
Run Code Online (Sandbox Code Playgroud)


问题

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

Eri*_*ert 18

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

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

此时运行更多代码可能会使事情变得更糟,而不是更好.发生这种情况时最安全的做法是检测情况并导致failfast在不运行任何代码的情况下关闭整个过程.你不知道在这一点上运行更多代码会有什么可怕的事情.

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

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

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

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

然后我创建一个锁对象并将其锁定在公共事件的添加和删除方法中,这些方法会更改名为"event_handlers"的私有委托变量.

当然,那没关系.我质疑该功能的必要性 - 我很少遇到多个线程正在向事件添加事件处理程序的情况 - 但我会接受你的话,因为你处于这种情况.

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

    lock (collection_lock)
        foreach (Delegate handler in event_delegate.GetInvocationList())
            try {handler.DynamicInvoke( args );}catch{}
Run Code Online (Sandbox Code Playgroud)

让我们考虑那里出了什么问题.

线程Alpha进入收集锁.

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

线程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{}
Run Code Online (Sandbox Code Playgroud)

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

并不是的.你把另一个问题换成了一个问题:

线程Alpha获取锁并制作委托列表的副本,并保留锁定.

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

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

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


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

您根本无法放弃确保事件处理程序正确的责任,所以不要尝试.编写事件处理程序以使它们正确 - 这样它们就可以正确地命令锁定并且永远不会抛出未处理的异常.

如果它们不正确并最终抛出异常,则立即取消该过程.不要因为尝试运行现在生活在不稳定过程中的代码而保持混乱.

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

  • @Triynko:你提出的建议就是说,嘿,当我在玛丽的办公室时,建筑物着火,继续从办公室到办公室,然后当所有的会议都完成后,在你的笔记中写下这座建筑着火了.正确的做法是要么*灭火*或*放弃所有当前的工作,保存你可以获得的信息,并打电话给消防部门*. (3认同)
  • @Triynko:我同意你的说法,破坏通知并不好,但问题是替代方案是否更糟.类比:假设您有一个业务流程,员工的管理链中的每个成员必须在促销发生之前亲自通知.一个合理的过程.现在假设在通知会议链中途,建筑物着火了.你是否忽略了火警并继续从一个办公室到另一个办公室,告诉副总统鲍勃将要升职,还是你撤离大楼? (2认同)
  • @configurator:我认为,他说的是,在长期锁定下运行任意代码不会导致死锁,因为长期持有的锁永远不会被争论.但是如果你已经保证锁定永远不会被争论那么*为什么首先要锁定*?整个场景搞砸了. (2认同)

Dav*_*sky 1

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