Цикл событий
В информатике, цикл событий [1] [2] [3] , диспетчер сообщений, цикл сообщений, помпа сообщений, или рабочий цикл — программная конструкция, которая ожидает прибытия и производит рассылку событий или сообщений в программе. Он работает, делая запрос к некоторому внутреннему или внешнему «поставщику событий» (который, как правило блокирует запрос до тех пор, пока событие не появится), а затем вызывает соответствующий обработчик события («отправляет событие»). Цикл событий может быть использован в сочетании с паттерном проектирования Reactor, если поставщик событий соответствует файловому интерфейсу, который может быть выбран (имеется в виду методом select) или «опрашивается» (имеется в виду системный вызов Unix, а не фактический опрос). Цикл событий почти всегда работает асинхронно с отправителем.
Когда цикл событий образует центральный поток управления, образующий программу, как это часто бывает, такой цикл может быть назван главным циклом или главным циклом событий. Это название подходит потому, что такой цикл событий находится на самом высоком уровне потока управления в рамках программы.
Передача сообщений
правитьПомпы сообщений, как говорится, «перекачивают» сообщения в программе из очереди сообщений (привязанной и, как правило, находящейся под управлением операционной системы) для обработки. В строгом смысле цикл событий является одним из методов для реализации связи между процессами. На самом деле обработка сообщений существует во многих системах, в том числе на уровне ядра операционной системы Mach. Цикл событий является специфической техникой реализации систем, использующих передачу сообщений.
Использование
правитьТрадиционно программы писались в синхронном стиле: всё взаимодействие с программой сводилось либо к передаче данных через аргументы командной строки, либо через ввод со стороны пользователя. Такой подход оказался не применим для написания программ, использующих графический интерфейс. Для таких программ удобнее использовать асинхронный стиль, где на определённые события вызывается зарегистрированный обработчик (функция). Для реализации такого подхода используется цикл событий. Обычно операционная система предоставляет функцию выборки следующего сообщения (наподобие get_next_message()
, и блокирует поток выполнения до тех пор пока, не появится хотя бы одно сообщение. Таким образом, тело цикла выполняется только тогда, когда есть что обрабатывать. Если для данного сообщения зарегистрирован обработчик — он вызывается. Как правило долгие операции, такие как взаимодействие с сетью, выполняются в других потоках выполнения, чтобы графический интерфейс оставался отзывчивым.
Цикл событий в псевдокоде:
function main initialize() while message != quit message := get_next_message() process_message(message) end while end function
Асинхронный подход нашел применение и в сетевом программировании. Например, сервер nginx, работающий асинхронно и использующий неблокирующий ввод-вывод, способен обрабатывать гораздо большее количество соединений, чем его синхронные аналоги, создающие по потоку или процессу на каждого клиента.
Реализации
правитьUnix
правитьВ Unix парадигма «всё это файл», естественно, приводит к циклу событий, в основе которого лежат события, связанные с файлами. Чтение и запись в файлы, межпроцессное взаимодействие, сети связи и управления устройствами – все это достигается с помощью файлового ввода/вывода, где файлы идентифицируются дескрипторами. Системные вызовы select
и poll
позволяют наблюдать за изменением состояния множества файловых дескрипторов, например, чтобы узнать, когда данные становятся доступными для чтения/записи, ошибками и прочими событиями, связанными с файлами. Данные вызовы блокируют выполнение программы на определённое время, пока на одном из наблюдаемых файловых дескрипторов не появится запрашиваемое событие. Данные функции замедляют работу при большом числе файловых дескрипторов, поэтому используются их более современные аналоги: epoll на Linux и kqueue на FreeBSD. Для всех этих вызовов следует использовать неблокирующие файловые дескрипторы.
Обработка сигналов
правитьОдна из немногих черт Unix, несоответствующих файловому интерфейсу, – это асинхронные события (сигналы). Сигналы, получаемые в обработчике сигнала, – это маленькие, ограниченные фрагменты кода, которые работают в то время, как остальная часть задачи приостановлена. Если сигнал принимается и обрабатывается, и при этом задача заблокирована в select()
, то select()
завершится преждевременно с EINTR. Если сигнал принимается во время выполнения кода в ЦП, то задача будет приостановлена между инструкциями, на то время, пока обработчик сигнала не завершится.
Таким образом, очевидный способ обрабатывать сигналы для обработчиков сигналов – это установка глобального флага и включение проверки этого флага в цикл событий непосредственно до и после вызова select()
, и, если он установлен, обрабатывать сигнал таким же образом, как и события с помощью дескрипторов. К сожалению, это приводит к состоянию гонки: если сигнал поступает сразу между проверкой флага и вызовом select()
, то он не будет обработан до тех пор, пока из select()
не будет произведён возврат, или по какой-либо другой причине (например, прерывании разочарованным пользователем).
Решением, к которому пришли в POSIX, является pselect, вызов которого похож на select()
, но имеет дополнительный sigmask
, описывающий маску сигналов. Это позволяет приложению замаскировать сигналы в основной задаче, а затем удалить маску в течение того времени, пока управление находится в вызове select()
, так что обработчики сигналов вызываются, только пока приложение находится на границе ввода-вывода. Тем не менее реализации pselect()
только в последнее время стали надежными; версии до Linux 2.6.16 не имеют системного вызова pselect()
, заставляя Glibc подражать ему с помощью метода, склонного к тому же состоянию гонки, во избежание которого и предназначен pselect()
.
Альтернативное и более переносимое решение заключается в преобразовании асинхронных событий в события на основе файлов, используя пайп-себе трюк,[4], в котором «обработчик сигнала пишет байт в пайп, другой конец которого наблюдается вызовом pselect()
в основной программе».[5] В ядре Linux версии 2.6.22 был добавлен новый системный вызов signalfd()
, который позволяет получать сигналы через специальный дескриптор файла.
Microsoft Windows
правитьПомимо неблокирующего ввода-вывода, использующего такие функции мультиплексирования ввода-вывода, как WSAPoll или select, в операционной системе Microsoft Windows предусмотрен и асинхронный ввод-вывод. Для асинхронных операций ввода-вывода существует Overlapped I/O. Если в блокирующие функции, такие как ReadFile() или WriteFile(), передать одним из аргументов структуру OVERLAPPED
, то эти функции мгновенно вернут управление программе. Узнать о завершении операции можно с помощью callback функции или Input/output completion port (рус. порт завершения ввода-вывода).
Помимо ввода-вывода в Windows реализован цикл событий для графических приложений. «Сердцем» таких приложений является функция WinMain(), которая вызывает GetMessage() в цикле. GetMessage() блокируется, пока не поступит какое-либо событие (также есть неблокирующая альтернатива в виде PeekMessage()). Далее, после небольшой обработки вызывается DispatchMessage(), которая передаёт сообщение о событии надлежащему обработчику, также известному как WindowProc. Сообщения, для которых не зарегистрирован обработчик, передаются обработчику по умолчанию (DefWindowProc)
См. также
правитьПримечания
править- ↑ Ретабоуил, Сильвен. Android NDK. Разработка приложений под Android на C/C++. — М.: ДМК Пресс, 2012. — С. 190. — 496 с. — ISBN 978-5-94074-657-7.
- ↑ Будилов Вадим Анатольевич. Интернет-программирование на Java. — Петербург: БХВ, 2003. — С. 41. — 704 с.
- ↑ Ламот, Андре. Программирование трехмерных игр для Windows. Советы профессионала по трехмерной графике и растеризации (+ CD-ROM) = Tricks of the 3D Game Programming Gurus: Advanced 3D Graphics and Rasterization. — М.: Вильямс, 2006. — С. 73. — 1415 с. — ISBN 5-8459-0627-X, 0-672-31835-0.
- ↑ D. J. Bernstein. The self-pipe trick . Дата обращения: 23 сентября 2015. Архивировано 3 сентября 2011 года.
- ↑ BUGS,
pselect(2)
: synchronous I/O multiplexing — страница справки man для разработчика Linux — системные вызовы (англ.)