В информатике цикл событий (также известный как диспетчер сообщений , цикл сообщений , насос сообщений или цикл выполнения ) — это программная конструкция или шаблон проектирования , который ожидает и отправляет события или сообщения в программе . Цикл событий работает, делая запрос некоторому внутреннему или внешнему «поставщику событий» (который обычно блокирует запрос до тех пор, пока не поступит событие), а затем вызывает соответствующий обработчик событий («отправляет событие»).
Он также широко применяется на таких серверах, как веб-серверы .
Цикл событий может использоваться совместно с реактором , если поставщик событий следует файловому интерфейсу, который может быть выбран или «опрошен» (системный вызов Unix, а не фактический опрос ). Цикл событий почти всегда работает асинхронно с отправителем сообщения.
Когда цикл событий формирует центральную конструкцию потока управления программы, как это часто бывает, его можно назвать основным циклом или основным циклом событий . Это название уместно, поскольку такой цикл событий находится на самом высоком уровне управления в программе.
Говорят, что насосы сообщений «перекачивают» сообщения из очереди сообщений программы (назначенной и обычно принадлежащей базовой операционной системе) в программу для обработки. В самом строгом смысле цикл событий является одним из методов реализации межпроцессного взаимодействия . Фактически, обработка сообщений существует во многих системах, включая компонент уровня ядра операционной системы Mach . Цикл событий является особой техникой реализации систем, использующих передачу сообщений .
Этот подход отличается от ряда других альтернатив:
Из-за преобладания графических пользовательских интерфейсов большинство современных приложений имеют основной цикл. get_next_message()
Процедура обычно предоставляется операционной системой и блокируется до тех пор, пока не будет доступно сообщение. Таким образом, цикл запускается только тогда, когда есть что-то для обработки.
функция основная инициализировать() пока сообщение != выйти сообщение := получить_следующее_сообщение() сообщение_процесса(сообщение) конец , пока функция конца
В Unix парадигма " все есть файл " естественным образом приводит к циклу событий на основе файлов. Чтение из файлов и запись в них, межпроцессное взаимодействие, сетевое взаимодействие и управление устройствами — все это достигается с помощью файлового ввода-вывода, при этом цель идентифицируется файловым дескриптором . Системные вызовы select и poll позволяют отслеживать набор файловых дескрипторов на предмет изменения состояния, например, когда данные становятся доступными для чтения.
Например, рассмотрим программу, которая считывает данные из постоянно обновляемого файла и отображает его содержимое в системе 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()
системного вызова, [1] принуждение glibc эмулировать его с помощью метода, подверженного тому же самому состоянию гонки, pselect()
призвано избежать этого.
Альтернативным, более портативным решением является преобразование асинхронных событий в файловые события с помощью трюка self-pipe , [2] где «обработчик сигнала записывает байт в канал, другой конец которого отслеживается select()
в основной программе». [3] В ядре Linux версии 2.6.22 был добавлен новый системный вызов signalfd()
, который позволяет получать сигналы через специальный файловый дескриптор.
Веб-страница и ее JavaScript обычно выполняются в однопоточном процессе веб-браузера . Процесс браузера обрабатывает сообщения из очереди по одному за раз. Функция JavaScript или другое событие браузера могут быть связаны с данным сообщением. Когда процесс браузера завершает работу с сообщением, он переходит к следующему сообщению в очереди.
В операционной системе Microsoft Windows процесс, взаимодействующий с пользователем, должен принимать и реагировать на входящие сообщения, что почти неизбежно выполняется циклом сообщений в этом процессе. В Windows сообщение приравнивается к событию, созданному и навязанному операционной системе. Событием может быть взаимодействие с пользователем, сетевой трафик, системная обработка, активность таймера, межпроцессное взаимодействие и т. д. Для неинтерактивных событий, связанных только с вводом-выводом, в Windows есть порты завершения ввода-вывода . Циклы портов завершения ввода-вывода выполняются отдельно от цикла сообщений и не взаимодействуют с циклом сообщений из коробки.
«Сердцем» большинства приложений Win32 является функция WinMain(), которая вызывает GetMessage() в цикле. GetMessage() блокируется до тех пор, пока не будет получено сообщение или «событие» (с функцией PeekMessage() в качестве неблокирующей альтернативы). После некоторой необязательной обработки она вызовет DispatchMessage(), который отправит сообщение соответствующему обработчику, также известному как WindowProc . Обычно сообщения, не имеющие специального WindowProc() , отправляются в DefWindowProc , который используется по умолчанию. DispatchMessage() вызывает WindowProc дескриптора HWND сообщения (зарегистрированного с помощью функции RegisterClass() ).
Более поздние версии Microsoft Windows гарантируют программисту, что сообщения будут доставлены в цикл сообщений приложения в том порядке, в котором они были восприняты системой и ее периферией. Эта гарантия имеет важное значение при рассмотрении последствий проектирования многопоточных приложений.
Однако некоторые сообщения имеют другие правила, например, сообщения, которые всегда принимаются последними, или сообщения с другим задокументированным приоритетом. [4]
Приложения X , напрямую использующие Xlib , построены вокруг XNextEvent
семейства функций; XNextEvent
блокирует до тех пор, пока событие не появится в очереди событий, после чего приложение обрабатывает его соответствующим образом. Цикл событий Xlib обрабатывает только системные события окна; приложения, которым необходимо иметь возможность ждать другие файлы и устройства, могут построить свой собственный цикл событий из примитивов, таких как ConnectionNumber
, но на практике, как правило, используют многопоточность .
Очень немногие программы используют Xlib напрямую. В более распространенном случае наборы инструментов GUI на основе Xlib обычно поддерживают добавление событий. Например, наборы инструментов на основе Xt Intrinsics имеют XtAppAddInput()
и XtAppAddTimeout()
.
Обратите внимание, что вызывать функции Xlib из обработчика сигналов небезопасно, поскольку приложение X могло быть прервано в произвольном состоянии, например, в XNextEvent
. См. [1] для решения для X11R5, X11R6 и Xt.
Цикл событий GLib изначально был создан для использования в GTK , но теперь используется и в не-GUI-приложениях, таких как D-Bus . Опрашиваемый ресурс — это набор файловых дескрипторов, в которых заинтересовано приложение; блок опроса будет прерван, если поступит сигнал или истечет тайм-аут (например, если приложение указало тайм-аут или задачу бездействия). Хотя GLib имеет встроенную поддержку для файловых дескрипторов и событий завершения дочерних процессов, можно добавить источник событий для любого события, которое может быть обработано в модели подготовки-проверки-отправки.[2]
Библиотеки приложений, построенные на цикле событий GLib, включают GStreamer и асинхронные методы ввода-вывода GnomeVFS , но GTK остается наиболее заметной клиентской библиотекой. События из оконной системы (в X , считываются с сокета X ) транслируются GDK в события GTK и выдаются как сигналы GLib на виджетных объектах приложения.
На поток допускается только один CFRunLoop, и может быть присоединено произвольное количество источников и наблюдателей. Затем источники взаимодействуют с наблюдателями через цикл выполнения, который организует постановку в очередь и отправку сообщений.
CFRunLoop абстрагирован в Cocoa как NSRunLoop, что позволяет ставить любое сообщение (эквивалентное вызову функции в нерефлексивных средах выполнения) в очередь для отправки любому объекту.