Реактор (шаблон проектирования)

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску

Шаблон проектирования программного обеспечения «реактор» — стратегия обработки событий, которая может одновременно реагировать на множество потенциальных запросов на обслуживание. Ключевым компонентом шаблона является цикл событий, работающий в одном потоке или процессе, который демультиплексирует входящие запросы и отправляет их нужному обработчику запросов.[1]

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

Благодаря балансу простоты и масштабируемости реактор стал центральным архитектурным элементом в нескольких серверных приложениях и фреймворков для работы в сети. Для особых случаев, когда необходима еще большая пропускная способность, производительность или сложность запроса, существуют производные — мультиреактор и проактор.[1][2][3][4]

Обзор[править | править код]

Первоначальной мотивацией для шаблона реактора были практические соображения по модели клиент-сервер в больших сетях, такие как проблема C10k для веб-серверов.[5]

Наивный подход к обработке запросов на обслуживание от многих потенциальных конечных точек, таких как сетевые сокеты или файловые дескрипторы, заключается в прослушивании новых запросов из цикла событий, и немедленном чтении самого раннего запроса. Как только весь запрос будет прочитан, его можно обработать и переслать, напрямую вызвав соответствующий обработчик. Подобный полностью «итеративный» сервер, обрабатывающий один запрос от начала до конца за цикл событий, логически правилен. Однако он отстанет, если получит несколько запросов подряд. Итеративный подход не может масштабироваться, поскольку чтение запроса блокирует единственный поток сервера до тех пор, пока не будет получен полный запрос. А операции ввода-вывода обычно выполняются намного медленнее, чем другие вычисления.[2]

Одной из стратегий преодоления этого ограничения является многопоточность: за счет немедленного отделения каждого нового запроса в отдельный рабочий поток, первый запрос больше не будет блокировать цикл событий, который может немедленно завершить итерацию и обработать другой запрос. Такая конструкция «поток на соединение» масштабируется лучше, чем чисто итеративная, но она содержит свои ограничения. С точки зрения базовых системных ресурсов каждый новый поток или процесс требует дополнительных затрат памяти и времени обработки (из-за переключения контекста). Фундаментальная неэффективность каждого потока, ожидающего завершения ввода-вывода, также не решена.[1][2]

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

  1. Оставить однопоточный обработчик событий; многопоточность приводит к накладным расходам и сложности, не решая реальную проблему блокировки ввода-вывода.
  2. Использовать механизм уведомления о событиях для демультиплексирования запросов только после завершения ввода-вывода (чтобы ввод-вывод фактически не блокировался).
  3. Зарегистрировать обработчики запросов как обратные вызовы с обработчиком событий для лучшего разделения задач.

Объединение этих идей ведет шаблону реактора, который сочетает в себе преимущества однопоточной обработки с высокой пропускной способностью и масштабируемостью.[1][2]

Применение[править | править код]

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

Однако у шаблона реактора есть ограничения, главным из которых является использование обратных вызовов, которые усложняют анализ и отладку программы — проблема, характерная для проектов с инвертированным управлением.[1] Более простые подходы: «поток на соединение» и полностью итеративный подход, позволяют избежать этого и могут быть приемлемыми решениями, если не требуется масштабируемость или высокая пропускная способность. [a]

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

Приложения[править | править код]

Шаблон реактора (или его вариант) нашел место во многих веб-серверах, серверах приложений и сетевых платформах:

Состав[править | править код]

Реактивное приложение состоит из нескольких активных частей и опирается на некоторые механизмы поддержки:[1]

Handle
Идентификатор и интерфейс к экземпляра запроса с IO и данными. Обычно представлен в форме сокета, файлового дескриптора, или похожего механизма предоставляемого большинством современных ОС.
Demultiplexer
Извещатель событий, который может эффективно наблюдать "статус" хендлов, и извещать суб-системы о изменениях "статуса" (типичное - что хендл-IO стал "готов к чтению"). Исторически эта роль заполнялась select() system call, или более современные примеры включают epoll, kqueue, and IOCP.
Dispatcher
Цикл обработки событий реактивного приложения. Этот компонент управляет регистраций обработчиков событий, которые вызываются на появление соответствующего события.
Event Handler
Обработчик запросов, реализует логику обработки специфическую для каждого типа запросов. Паттерн реактора полагается на динамическую регистрацию его диспетчере в виде обратных вызовов, для большей гибкости. По умолчанию, реактор не использует мульти-поточность, но вызывает обработчик запроса в нитке диспетчера.
Event Handler Interface
Абстрактный класс-интерфейс, предоставляет основные свойства и методы обработчика события. Конкретный обработчик реализует интерфейс, а диспетчер посредством его оперирует обработчиком события.

Варианты[править | править код]

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

Одна из основных модификаций — вызывать обработчики событий в их собственных потоках для большей параллелизма. Запуск обработчиков в пуле потоков вместо запуска новых потоков по мере необходимости еще больше упростит многопоточность и минимизирует накладные расходы. Т. о. пул потоков — естественное дополнение шаблона реактора во многих случаях использования.[2]

Другой способ максимизировать пропускную способность — частично воспроизвести подход сервера «поток на соединение» с размножаемыми параллельными диспетчерами/циклами событий. Однако вместо количества соединений можно настроить количество диспетчеров, чтобы оно соответствовало доступным ядрам ЦП базового оборудования.

Этот вариант известен как мультиреактор. Он гарантирует, что выделенный сервер полностью использует вычислительную мощность оборудования. Поскольку отдельные потоки представляют собой долгоживущие циклы событий, накладные расходы на создание и уничтожение потоков ограничиваются запуском и завершением работы сервера. Поскольку запросы распределяются между независимыми диспетчерами, мультиреактор также обеспечивает лучшую доступность и надежность; в случае возникновения ошибки и сбоя одного диспетчера он будет прерывать только запросы, выделенные этому циклу событий.[3][4]

Для особенно сложных сервисов, одновременно требующих синхронности и асинхронности, еще одной альтернативой является шаблон проактора. Этот шаблон более сложен, чем реактор, со своими инженерными деталями, но он по-прежнему использует подкомпонент реактора для решения проблемы блокировки ввода-вывода.[3]

См. также[править | править код]

Связанные шаблоны:

Примечания[править | править код]

  1. Практическое правило софто-строения говорит что если требования к приложению потенциально могут увеличить существующие и предполагаемые лимиты, можно ожидать что однажды так и будет.
  1. 1 2 3 4 5 6 7 8 9 10 11 Schmidt, Douglas C. Chapter 29: Reactor: An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events // Pattern Languages of Program Design / Coplien. — 1st. — Addison-Wesley, 1995. — Vol. 1. — ISBN 9780201607345.
  2. 1 2 3 4 5 6 7 Devresse. Efficient parallel I/O on multi-core architectures. 2nd Thematic CERN School of Computing. CERN (20 июня 2014). Дата обращения: 14 сентября 2023. Архивировано 8 августа 2022 года.
  3. 1 2 3 4 5 Escoffier, Clement. Chapter 4. Design Principles of Reactive Systems // Reactive Systems in Java / Clement Escoffier, Ken Finnegan. — O'Reilly Media, November 2021. — ISBN 9781492091721.
  4. 1 2 3 Garrett. Inside NGINX: How We Designed for Performance & Scale. NGINX. F5, Inc. (10 июня 2015). Дата обращения: 10 сентября 2023. Архивировано 20 августа 2023 года.
  5. Kegel. The C10k problem. Dan Kegel's Web Hostel (5 февраля 2014). Дата обращения: 10 сентября 2023. Архивировано 6 сентября 2023 года.
  6. Network Programming: Writing network and internet applications. POCO Project 21–22. Applied Informatics Software Engineering GmbH (2010). Дата обращения: 20 сентября 2023.
  7. Stoyanchev. Reactive Spring. Spring.io (9 февраля 2016). Дата обращения: 20 сентября 2023.

Ссылки[править | править код]

Конкретные приложения:

Примеры реализации: