C# 关于触发和处理.NET事件的最佳实践

C# 关于触发和处理.NET事件的最佳实践,c#,multithreading,events,asynchronous,deadlock,C#,Multithreading,Events,Asynchronous,Deadlock,很明显,在锁(即关键部分)内触发事件容易出现死锁,因为事件处理程序可能会阻止某些也需要获取相同锁的异步操作。现在,在设计方面,我想到了两种可能的解决方案: 如果需要在锁内触发事件,则始终异步触发事件。例如,如果事件的触发顺序无关紧要,则可以使用ThreadPool执行此操作。如果必须保留事件的顺序,则可以使用单个事件触发线程按顺序但异步地触发事件 触发事件的类/库不需要采取任何必要的预防措施来防止死锁,只需在锁内触发事件即可。在这种情况下,如果事件处理程序在事件处理程序内执行锁定(或任何其他阻止

很明显,在锁(即关键部分)内触发事件容易出现死锁,因为事件处理程序可能会阻止某些也需要获取相同锁的异步操作。现在,在设计方面,我想到了两种可能的解决方案:

  • 如果需要在锁内触发事件,则始终异步触发事件。例如,如果事件的触发顺序无关紧要,则可以使用ThreadPool执行此操作。如果必须保留事件的顺序,则可以使用单个事件触发线程按顺序但异步地触发事件

  • 触发事件的类/库不需要采取任何必要的预防措施来防止死锁,只需在锁内触发事件即可。在这种情况下,如果事件处理程序在事件处理程序内执行锁定(或任何其他阻止操作),则事件处理程序负责异步处理事件。如果事件处理程序不符合此规则,则在发生死锁时,它应该承担后果

  • 老实说,我认为第二个选项在关注点分离原则方面更好,因为事件触发代码不应该猜测事件处理程序可以做什么或不可以做什么

    然而,实际上,我倾向于采用第一种方法,因为第二种方法似乎会收敛到每个事件处理程序现在都必须异步运行所有事件处理代码的程度,因为大多数时候不清楚某些调用序列是否执行阻塞操作。对于复杂的事件处理程序,跟踪所有可能的路径(而且,随着代码的发展跟踪路径)绝对不是一项容易的任务。因此,在一个地方(触发事件的地方)解决问题似乎更可取

    我很想知道是否有我可能忽略的其他替代解决方案,以及每种可能的解决方案可能有哪些优点/缺点和缺陷


    对于这种情况是否有最佳做法?

    还有第三种选择:延迟引发事件,直到释放锁。通常,锁的使用时间很短。通常可以将引发事件延迟到锁定之后(但在同一线程上)

    BCL几乎从不在锁下调用用户代码。这是他们明确的设计原则。例如,
    ConcurrentDictionary.AddOrUpdate
    在锁定状态下不调用工厂。这种违反直觉的行为会导致许多堆栈溢出问题,因为它会导致对同一个键进行多个工厂调用

    老实说,我认为第二个选项在关注点分离原则方面更好,因为事件触发代码不应该猜测事件处理程序可以做什么或不可以做什么

    我不认为第一种选择违反了关注点分离。提取一个
    AsyncEventNotifier
    类,并将事件生成对象委托给它,类似(显然不完整):

    类AsyncEventNotifier
    {
    私人列表监听器;
    public void AddEventListener(EventListener){{u listeners.add(listener);}
    public void NotifyListeners(EventArgs args)
    {
    //生成一个新线程来调用侦听器方法
    }
    ....
    }
    类事件生成类
    {
    私有AsyncEventHandler\u AsyncEventHandler;
    public void AddEventListener(EventListener侦听器){{u asyncEventHandler.AddEventListener(侦听器);}
    私有void FireSomeEvent()
    {
    var eventArgs=新的eventArgs();
    ...
    _asyncEventhandler.NotifyListeners(eventArgs);
    }
    }
    

    现在,您原来的类不负责以前不负责的任何事情。它知道如何向自身添加侦听器,并且知道如何告诉侦听器已发生事件。新类知道“告诉其侦听器已发生事件”的实现细节。如果重要的话,听者顺序也会被保留。不过,可能不应该这样。如果listenerB在listenerA处理事件之前无法处理该事件,则listenerB可能对listenerA生成的事件比对其侦听的对象更感兴趣。或者您应该有另一个类,其职责是了解事件需要处理的顺序。

    我完全同意,这是另一种可能的解决方案。虽然很容易看出触发事件的方法是否有锁,但通常很难或不可能知道其中一个调用方法(在导致事件触发方法的调用链中)是否已经有锁。在这种情况下,很难在锁上下文之外触发事件。BCL是一个相关的例子,但我不确定它是否适用于具有复杂业务逻辑的应用程序代码,因为BCL只是一个独立(通常较小且非线程安全)类的集合。@exc您是对的:如果您的代码在锁定区域大量使用锁定和深层调用堆栈,这将是一个大问题。我想建议您重新评估这样复杂的线程代码是否真的是最好的解决方案?!特别是考虑到死锁是最致命的错误,因为如果没有人工干预,应用程序永远无法恢复。;在您的情况下,将回调调用推送到线程池可能是安全且容易的。这是我的新答案。我真诚地感谢您提交的答案,并希望指出,我原则上同意您的观点,并且我非常努力地避免编写这种不可管理的代码。但是,请记住,这种理想化的方法通常不适用于拥有大量代码库(数百万行代码)的情况,并且大多数代码都是由数十人编写的遗留代码,目前由一群团队维护
    class AsyncEventNotifier
    {
        private List<EventListener> _listeners;
    
        public void AddEventListener(EventListener listener) { _listeners.add(listener); }
        public void NotifyListeners(EventArgs args)
        {
            // spawn a new thread to call the listener methods
        }
        ....
    }
    
    class EventGeneratingClass
    {
        private AsyncEventHandler _asyncEventHandler;
    
        public void AddEventListener(EventListener listener) { _asyncEventHandler.AddEventListener(listener); }
    
        private void FireSomeEvent()
        {
            var eventArgs = new EventArgs();
            ...
            _asyncEventhandler.NotifyListeners(eventArgs);
        }
    }