Передовой опыт в отношении запуска и обработки событий .NET

Очевидно, что запуск событий внутри блокировки (т. е. в критической секции) может привести к взаимоблокировкам из-за возможности того, что обработчик событий может заблокировать какую-либо асинхронную операцию, которая также должна получить ту же блокировку. Теперь, с точки зрения дизайна, есть два возможных решения, которые приходят мне на ум:

  1. Если необходимо запустить событие внутри блокировки, всегда запускайте событие асинхронно. Это можно сделать, например, с помощью ThreadPool, если порядок запуска событий не имеет значения. Если порядок событий должен быть сохранен, то можно использовать один поток запуска событий для запуска событий по порядку, но асинхронно.

  2. Классу/библиотеке, запускающей события, не нужно предпринимать никаких необходимых мер предосторожности, чтобы предотвратить взаимоблокировку, и просто запускать событие внутри блокировки. В этом случае ответственность за асинхронную обработку события лежит на обработчике событий, если он выполняет блокировку (или любую другую операцию блокировки) внутри обработчика событий. Если обработчик события не соответствует этому правилу, он должен пострадать от последствий в случае возникновения взаимоблокировки.

Честно говоря, я считаю, что второй вариант лучше с точки зрения принципа разделения ответственности, поскольку код запуска события не должен догадываться, что может или не может делать обработчик события.

Однако на практике я склоняюсь к первому пути, поскольку второй вариант, похоже, сводится к тому, что каждый обработчик событий должен теперь выполнять весь код обработки событий асинхронно, поскольку в большинстве случаев неясно, выполняется ли какая-либо серия вызовов. выполняет операцию блокировки или нет. Для сложных обработчиков событий отслеживание всех возможных путей (и, более того, отслеживание их по мере развития кода) — определенно непростая задача. Поэтому решение проблемы в одном месте (там, где запускается событие) представляется предпочтительным.

Мне интересно посмотреть, есть ли другие альтернативные решения, которые я мог упустить, и какие возможные преимущества/недостатки и подводные камни можно отнести к каждому возможному решению.

Есть ли лучшая практика для такой ситуации?


person exc    schedule 09.07.2014    source источник


Ответы (2)


Есть и третий вариант: отложить вызов события до снятия блокировки. Обычно замки берутся на короткое время. Обычно можно отложить вызов события до блокировки (но в том же потоке).

BCL почти никогда не вызывает пользовательский код под замком. Это явный принцип их дизайна. Например, ConcurrentDictionary.AddOrUpdate не вызывает фабрику, находясь под замком. Это нелогичное поведение вызывает много вопросов о переполнении стека, поскольку может привести к нескольким вызовам фабрики для одного и того же ключа.

person usr    schedule 09.07.2014
comment
Я определенно согласен, это еще одно возможное решение. Хотя очень легко просто увидеть, есть ли блокировка в методе, запускающем событие, обычно очень сложно или невозможно узнать, является ли один из вызывающих методов (в цепочке вызовов, ведущей к методу запуска события) уже есть замок. В таких случаях очень сложно запустить событие вне контекста блокировки. BCL — подходящий пример, но я не уверен, применим ли он к коду приложения со сложной бизнес-логикой, поскольку BCL — это просто набор независимых (обычно небольших и не потокобезопасных) классов. - person exc; 10.07.2014
comment
@exc вы правы: если ваш код интенсивно использует блокировки и глубокие стеки вызовов в заблокированном регионе, это большая проблема. Я хотел бы предложить, что, возможно, вам следует переоценить, действительно ли такой сложный многопоточный код является лучшим решением?! Особенно с учетом того, что взаимоблокировки являются наиболее фатальными ошибками, поскольку приложение никогда не восстанавливается без ручного вмешательства.; В вашем случае, вероятно, безопасно и легко отправить вызов обратного вызова в пул потоков. Это мой новый ответ. - person usr; 10.07.2014
comment
Я искренне благодарю вас за ответ и хотел бы отметить, что я согласен с вами в принципе, и я очень стараюсь не писать такой неуправляемый код. Однако имейте в виду, что такой идеалистический подход обычно малоприменим, когда у вас есть большая кодовая база (миллионы строк кода), и большая ее часть представляет собой устаревший код, который был написан десятками людей и в настоящее время поддерживается группа не менее 15 человек. Я пытаюсь постепенно улучшать код, насколько это возможно, и, следовательно, мой вопрос. - person exc; 10.07.2014
comment
@exc конечно, я полностью это понимаю. Подход с задержкой, вероятно, неприменим. Ни один из них не переписывает кодовую базу. - person usr; 10.07.2014

Честно говоря, я считаю, что второй вариант лучше с точки зрения принципа разделения ответственности, поскольку код запуска события не должен догадываться, что может или не может делать обработчик события.

Я не думаю, что первый вариант нарушает разделение интересов. Извлеките класс AsyncEventNotifier и делегируйте ему свой объект, генерирующий события, что-то вроде (очевидно, неполного):

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

Теперь ваш первоначальный класс не несет ответственности ни за что, за что он не отвечал раньше. Он знает, как добавить слушателей к себе, и знает, как сообщить своим слушателям, что произошло событие. Новый класс знает детали реализации «сообщить своим слушателям, что произошло событие». Порядок слушателей также сохраняется, если это важно. Хотя, наверное, не должно быть. Если listenerB не может обработать событие до тех пор, пока listenerA уже не обработает его, тогда listenerB, вероятно, больше заинтересован в событии, сгенерированном listenerA, чем в объекте, который он прослушивал. Либо так, либо у вас должен быть другой класс, который должен знать порядок, в котором должно обрабатываться событие.

person Mike B    schedule 09.07.2014
comment
Спасибо за ваш ответ, и мне жаль, если я был недостаточно ясен, но это именно то, что я пытался описать в первом варианте. Причина, по которой я упомянул SoC, заключается в том, что в первую очередь у кода должна быть причина запускать событие асинхронно. Зачем запускать событие асинхронно? Потому что обработчики могут заблокировать. Однако действительно ли метод стрельбы должен это учитывать? Вот почему я упомянул SoC. Кстати, под порядком обработки событий я имел в виду порядок срабатывания разных событий относительно друг друга, а не порядок обработки одного события разными обработчиками. - person exc; 10.07.2014