воскресенье, 27 декабря 2009 г.

Обработка событий - что и как получать?

Варианты именно реализации подписки и обработки в самих потребителях событий здесь не буду рассматривать: все эти наследования реализации, наследования интерфейсов, утиные типизации, - все их плюсы и минусы уже неоднократно обсуждались и потому не вижу особого смысла обговаривать эти аспекты снова.

В этом же посте хочу сосредоточится на таком небольшом пункте, как аргументы в функциях-обработчиках событий. Вещь-то сейчас кажется тривиальной, но надо посмотреть на грабли, которые заложили для нас предшественники. Итак поехали:

При обработке событий можно выделить 2 аспекта: как мы получаем событие и что мы получаем с событием вместе.

При получении событий от источника сообщений возможны следующие варианты:
  • мы подписываемся отдельными потребителями на сообщения от соответствующих объектов источника
  • мы подписываемся на сообщения от всей системы-источника сообщений(при этом можно указать, что такие-то события нам нужны, а такие-то нет, но источником событий у нас служит система целиком)
В варианте с целевой подпиской мы не тратим время на фильтрацию сообщений и не ломаем голову, что мы что-то куда-то не туда отправили на обработку. В варианте с общей подпиской мы твёрдо уверены что ничего не пропустили. Т.е. у каждого подхода есть свои плюсы и минусы, впрочем как всегда...

Причины приведённые выше будут влиять на архитектурные решения, какой из вариантов выбирать: вариант с целевой подпиской выгоден в пределах одной системы и нескольких подсистем(продукты одной фирмы будут прекрасным примером реализации доставки сообщений), вариант с общей подпиской выгоден при стыковке сильно разнородных систем (практически все стандартные протоколы: MAPI, TAPI и т.п.)

Но, в целом, особой разницы между ними нет, скорее это разграничение на количество каналов, по которым общаются система источник событий и система потребитель событий - если канал 1, то у нас вариант с общей подпиской, если несколько, то мы скорее всего имеем вариант с целевой подпиской. И выбор какой из этих 2-х вариантов реализовывать больше дело вкуса.

Примером варианта с целевой подпиской может служить реализация доставки событий в COM, а примером с общей подпиской - WinAPI.

При предоставлении дополнительной информации сейчас наибольшее распространение получили два подхода:
  1. мы передаём все данные о произошедшем событии в функцию обработчик события, надеясь, что разработчику будет в дальнейшем этого достаточно.
  2. мы ничего не передаём, кроме собственно самого извещения, что событие произошло - с надеждой что разработчик получит всю нужную ему информацию сам.
А вот в этих-то зарослях и спрятаны основные грабли:

В варианте №1 ОЧЕНЬ часто забывают добавить константность для передаваемых параметров. И тогда следующий зарегистрированый обработчик может получить отнюдь не то, что ожидалось. И вообще, строить механизм вычислений с помощью обработчиков событий - не совсем хорошая идея ;)

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

А самые заботливо укрытые грабли лежат в в варианте №2, когда общение между системой источником и системой потребителем идёт по 1-му каналу - заботливо укрывает эти грабли тот факт, что их не видно в однопоточном приложении. Но вот если в системе крутится несколько потоков, причём два и больше потоков могут отправлять события, то тут всё и начинается.
Пример:
Есть система с 2-я потоками, каждый из которых изменяет некоторую структуру данных в памяти, в целях потокобезопасности доступ к структуре закрыт критическими секциями, события отправляются после каждого изменения, но в целях быстродействия отправка событий сделана асинхронно. Потребитель событий должен забрать данные из этой же структуры данных - доступ опять же организован безопасно. На события подписан только 1 потребитель. В определённый момент времени происходит следующая ситуация: поток1 получил доступ к данным, успешно их поменял и отправил извещение, что он закончил работу; поток2 со спокойной цифровой душой получает доступ к данным, тоже их изменяет и тоже отправляет сообщение "всё ок - принимайте данные". Потребитель событий начинает забирать даныые по 1-му отправленому извещению, но то ли такт процессорный неудачно лёг, то ли sleep студент поставил, ну в общем не успел забрать данные до того как поток2 их изменил. И вот что делать в такой ситуации? - событие мы обрабатываем мы ещё 1-е, а данные уже лежат для 2-го события.
Т.е. у нас классический Data Race заботливо подготовленный системой, которая отправляет события.
Решить эту ситуацию можно сделав структуру данных, к которой имеют доступ источник событий и их потребитель, неизменной для потребителя пока он не закончит обработку события - это либо передавать в виде параметра в функцию обработчик (на что никто не пойдёт, т.к. сигнатура функций не менялась уже лет 10), либо блокировать доступ к структуре данных пока все обработчики не закончат, на что опять же никто не пойдёт - т.к. это существенно замедлит работу основной системы.

Уменьшить вероятность проблем можно запустив отдельный поток, который будет обрабатывать сообщение и копировать структуру данных как можно ближе к моменту запуска сообщения. А функции-обработчики событий будут работать как в варианте №1. Но это не решит саму проблему с подходом №2 - просто уменьшит вероятность возникновения Data Race.

Выглядит описанная ситуация довольно надуманной, но это описание работы с реализацией TAPI от Cisco. :(

Как общее итого
не лениться и гонять всю необходимую инфу через параметры события - граблей меньше и они значительно менее опасны.