Цикл событий

Цикл событий (англ. event loop) — это алгоритм, используемый в программном обеспечении для непрерывной обработки и маршрутизации потока управления для событий. Цикл событий запрашивает следующее событие у поставщика событий (обычно при этом происходит блокировка до появления события), и по получении события вызывает связанный с ним обработчик событий. Когда цикл событий выступает в качестве центрального диспетчера событий программы, как это часто бывает, его называют главным циклом или главным циклом событий.

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

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

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

loop
    message := get_next_message()
    process_message(message)
while message != quit

Альтернативы

Во многих программах цикл событий включён в высокоуровневую структуру — как основной цикл. Однако существуют альтернативные архитектурные решения.

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

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

Примеры

HTML/JavaScript

Веб-страница и её JavaScript-код обычно выполняются в одном потоке процесса веб-браузера. Процесс браузера поочерёдно обрабатывает сообщения из очереди. Определённое сообщение может быть связано с функцией JavaScript или иным событием браузера. Закончив обработку сообщения, браузер переходит к следующему элементу в очереди.

Приложения в Windows

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

Основная функция большинства Win32-приложений — WinMain(), которая вызывает в цикле функцию GetMessage(). GetMessage() блокирует выполнение до получения сообщения (существует неблокирующая альтернатива — PeekMessage()). После дополнительной обработки вызывается DispatchMessage(), которая перенаправляет сообщение соответствующему обработчику — WindowProc. Обычно сообщения, не имеющие специальной обработки в пользовательской WindowProc(), направляются на стандартную функцию DefWindowProc. DispatchMessage() вызывает функцию WindowProc для окна, указанного в HWND-дескрипторе сообщения (данное соответствие определяется при регистрации окна с помощью RegisterClass()).

В современных версиях Windows гарантируется доставка сообщений основному циклу приложения в том порядке, в котором они были получены системой и периферийными устройствами. Эта гарантия важна при проектировании многопоточных приложений. Однако некоторые сообщения имеют иное поведение — например, всегда обрабатываются последними или обладают особым приоритетом[1].

Цикл событий Xlib

Приложения X, непосредственно использующие Xlib, строятся вокруг функций семейства XNextEvent; функция XNextEvent блокирует поток до появления события в очереди событий, после чего приложение его обрабатывает. Цикл событий Xlib обрабатывает только события оконной системы; приложениям, которым нужно ожидать данные от других файлов или устройств, может потребоваться собственный цикл событий с использованием примитивов вроде ConnectionNumber, однако на практике чаще используются многопоточность.

Прямое использование Xlib встречается редко. Обычно графические подсистемы, основанные на Xlib, поддерживают добавление событий. Например, библиотеки на базе Xt Intrinsics предоставляют функции XtAppAddInput() и XtAppAddTimeout().

Не рекомендуется вызывать функции Xlib из обработчиков сигналов, так как приложение может быть прервано в произвольном состоянии, например, во время исполнения XNextEvent. Решение для X11R5, X11R6 и Xt см. [1].

Цикл событий GLib

Цикл событий GLib изначально разрабатывался для использования в GTK, однако применяется и в других приложениях, в том числе не имеющих GUI, таких как D-Bus. Как ресурс для опроса используется набор дескрипторов файлов, представляющих интерес для приложения; цикл прерывается по поступлению сигнала или истечению таймаута (например, если задана зависимая от времени задача или задача ожидания). В GLib реализована поддержка событий по файловым дескрипторам и завершения дочерних процессов, а также возможно добавление произвольных источников событий, пригодных для модели prepare-check-dispatch[2].

На основе цикла событий GLib построены такие библиотеки, как GStreamer и асинхронные механизмы GnomeVFS, но GTK остаётся наиболее заметным клиентом. События от оконной системыX — данные поступают по сокету) преобразуются с помощью GDK в события GTK и генерируют сигналы GLib на виджетах приложения.

Основной цикл run loop в macOS Core Foundation

Для каждого потока разрешён ровно один CFRunLoop, к которому могут присоединяться произвольное количество источников и наблюдателей. Источники затем взаимодействуют с наблюдателями посредством цикла run loop, который организует постановку сообщений в очередь и их обработку.

CFRunLoop абстрагирован в Cocoa как NSRunLoop, благодаря чему возможно поставить в очередь для отправки любому объекту произвольное сообщение (эквивалент обращения к функции в нерефлексивных средах выполнения).

Основанные на файлах

В Unix парадигма «всё — файл» приводит к построению цикла событий, основанного на файлах. Чтение и запись файлов, межпроцессное и сетевое взаимодействие, а также управление устройствами реализуются через файловый ввод-вывод, а объект идентифицируется с помощью дескриптор файла. Системные вызовы select и poll используются для слежения за изменением состояния дескрипторов — например, при появлении новых данных для чтения.

Пример работы: программа читает содержимое постоянно обновляемого файла и отображает его в X Window System, взаимодействуя с сервером X Window по сокету (будь то Unix-доменный или Berkeley-сокет):

def main():
    file_fd = open("logfile.log")
    x_fd = open_display()
    construct_interface()
    while True:
        rlist, _, _ = select.select([file_fd, x_fd], [], []):
        if file_fd in rlist:
            data = file_fd.read()
            append_to_display(data)
            send_repaint_message()
        if x_fd in rlist:
            process_x_messages()

Основанные на сигналах

В Unix сигнал — это асинхронное событие, которое обрабатывается обработчиком сигналов, выполняющимся при приостановке основной задачи. Если сигнал был получен и обработан, пока задача блокировалась на select(), select завершится досрочно с кодом EINTR; если сигнал был получен во время выполнения процессора, задача приостанавливается между инструкциями до возврата обработчика.

Один из способов обработать сигнал — использовать глобальный флаг, который устанавливается обработчиком сигналов и проверяется циклом событий непосредственно до и сразу после вызова select(); в случае установки обрабатывать сигнал так же, как событие на файловом дескрипторе. Однако это приводит к появлению состояния гонки: если сигнал поступает непосредственно между проверкой флага и вызовом select(), он не будет обработан до следующего возврата select (например, при следующем событии ввода/вывода или прерывании).

В POSIX для этого реализован вызов pselect(), схожий с select, но с дополнительным параметром sigmask — маской сигналов. Это позволяет временно маскировать сигналы в основном потоке, а затем немаскировать их только на время вызова select(), чтобы обработчики сигналов вызывались только при операциях ввода-вывода. Однако реализации pselect() были не всегда надёжными; например, версии Linux до 2.6.16 не реализуют системный вызов pselect()[3], из-за чего glibc имитирует его посредством метода, подверженного тем же состояниям гонки, которые pselect() должен предотвращать.

Альтернативное, более переносимое решение — преобразовывать асинхронные события в событийные на файле с помощью «трюка с самописным каналом»:[4] обработчик сигналов записывает байт в канал, за вторым концом которого следит основной цикл через select()[5]. В Linux версии 2.6.22 был добавлен системный вызов signalfd(), позволяющий принимать сигналы через специальный дескриптор файла.

Примечания

  1. Функция GetMessage() и приоритеты сообщений. Архивировано 5 мая 2008 года.
  2. The Main Event Loop (официальная документация GLib). Архивировано 2 ноября 2011 года.
  3. Linux_2_6_16 - Linux Kernel Newbies (англ.). kernelnewbies.org. Дата обращения: 3 марта 2021. Архивировано 28 сентября 2025 года.
  4. D. J. Bernstein. The self-pipe trick (англ.). Дата обращения: 21 июня 2024. Архивировано 7 октября 2025 года.
  5. BUGS, pselect(2): синхронное мультиплексирование ввода-вывода — страница справки man для разработчика Linux — системные вызовы  (англ.)

Категории