В чем разница между epoll, poll, threadpool?
Может ли кто-нибудь объяснить, в чем разница между epoll
, poll
а нить пул?
- Какие плюсы / минусы?
- Любые предложения для рамок?
- Любые предложения для простых / базовых уроков?
- Кажется, что
epoll
а такжеpoll
специфичны для Linux... Есть ли эквивалентная альтернатива для Windows?
1 ответ
Threadpool на самом деле не входит в ту же категорию, что и poll и epoll, поэтому я предполагаю, что вы ссылаетесь на threadpool, как в "threadpool для обработки многих соединений с одним потоком на соединение".
Плюсы и минусы
- ThreadPool
- Разумно эффективен для малого и среднего параллелизма, может даже превзойти другие методы.
- Использует несколько ядер.
- Не масштабируется далеко за пределы "нескольких сотен", хотя некоторые системы (например, Linux) в принципе могут нормально планировать 100000 потоков.
- Наивная реализация демонстрирует проблему " громового стада ".
- Помимо переключения контекста и громового стада, нужно учитывать память. Каждый поток имеет стек (как правило, не менее мегабайта). Поэтому тысячи потоков занимают гигабайт оперативной памяти только для стека. Даже если эта память не выделена, она все равно отнимает значительное адресное пространство в 32-битной ОС (на самом деле проблема не в 64-битной версии).
- Темы действительно могут использовать
epoll
хотя и очевидным способом (все потоки блокируются наepoll_wait
) бесполезен, потому что epoll будет пробуждать все ожидающие его потоки, поэтому у него все равно будут те же проблемы.- Оптимальное решение: один поток прослушивает epoll, выполняет мультиплексирование ввода и передает запросы в пул потоков.
futex
Ваш друг здесь, в сочетании, например, с ускоренной очередью на поток. Хотя плохо документировано и громоздко,futex
предлагает именно то, что нужно.epoll
может возвращать несколько событий одновременно, иfutex
позволяет эффективно и точно контролировать пробуждение N заблокированных потоков одновременно (Nmin(num_cpu, num_events)
в идеале), и в лучшем случае это вообще не требует дополнительного переключения системного вызова / контекста.- Не тривиально для реализации, немного заботится.
fork
(он же старомодный пул потоков)- Разумно эффективен для малого и среднего параллелизма.
- Не масштабируется далеко за пределы "нескольких сотен".
- Переключение контекста намного дороже (разные адресные пространства!).
- Значительно хуже масштабируется на старых системах, где fork намного дороже (полная копия всех страниц). Даже на современных системах
fork
не "свободен", хотя накладные расходы в основном объединяются механизмом копирования при записи. На больших наборах данных, которые также модифицируются, значительное число ошибок страницfork
может негативно повлиять на производительность. - Тем не менее, доказано, что надежно работает более 30 лет.
- Смешно легко реализовать и твердая: если какой-либо из процессов рушится, мир не заканчивается. Нет (почти) ничего, что вы можете сделать неправильно.
- Очень склонен к "гремящему стаду".
poll
/select
- Два вкуса (BSD против System V) более или менее одно и то же.
- Несколько старое и медленное, несколько неловкое использование, но практически нет платформы, которая их не поддерживает.
- Ожидание, пока "что-то не случится" с набором дескрипторов.
- Позволяет одному потоку / процессу обрабатывать много запросов одновременно.
- Нет многоядерного использования.
- Необходимо каждый раз копировать список дескрипторов из пространства пользователя в ядро. Необходимо выполнить линейный поиск по дескрипторам. Это ограничивает его эффективность.
- Не масштабируется до "тысяч" (на самом деле жесткий предел составляет около 1024 в большинстве систем или до 64 в некоторых).
- Используйте его, потому что он переносим, если вы все равно имеете дело с дюжиной дескрипторов (без проблем с производительностью), или если вы должны поддерживать платформы, у которых нет ничего лучше. Не используйте иначе.
- Концептуально сервер становится немного сложнее, чем разветвленный, поскольку теперь вам нужно поддерживать множество соединений и конечный автомат для каждого соединения, и вы должны мультиплексировать между запросами по мере их поступления, собирать частичные запросы и т. Д. Простой разветвленный Сервер просто знает об одном сокете (ну, два, считая сокет прослушивания), читает, пока не получит то, что хочет, или пока соединение не будет наполовину закрыто, а затем записывает все, что хочет. Он не беспокоится ни о блокировке, ни о готовности, ни о голоде, ни о каких-то несвязанных данных, это проблема другого процесса.
epoll
- Только для Linux
- Концепция дорогих модификаций против эффективных ожиданий:
- Копирует информацию о дескрипторах в пространство ядра при добавлении дескрипторов (
epoll_ctl
)- Обычно это случается редко.
- Не нужно копировать данные в пространство ядра при ожидании событий (
epoll_wait
)- Обычно это происходит очень часто.
- Добавляет официанта (или, скорее, его структуру epoll) в очереди ожидания дескрипторов
- Поэтому дескриптор знает, кто слушает, и напрямую сигнализирует официантам, когда это уместно, а не официантам, ищущим список дескрипторов.
- Противоположный способ как
poll
работает - O(1) с небольшим k (очень быстро) относительно числа дескрипторов вместо O(n)
- Копирует информацию о дескрипторах в пространство ядра при добавлении дескрипторов (
- Очень хорошо работает с
timerfd
а такжеeventfd
(потрясающее разрешение и точность таймера тоже). - Хорошо работает с
signalfd
устраняя неловкую обработку сигналов, делая их частью нормального потока управления очень элегантным способом. - Экземпляр epoll может рекурсивно размещать другие экземпляры epoll
- Предположения, сделанные этой моделью программирования:
- Большинство дескрипторов большую часть времени простаивают, но несколько вещей (например, "данные получены", "соединение закрыто") фактически происходят с несколькими дескрипторами.
- В большинстве случаев вы не хотите добавлять / удалять дескрипторы из набора.
- Большую часть времени вы ждете, чтобы что-то произошло.
- Некоторые незначительные подводные камни:
- Эполл, запускаемый уровнем, пробуждает все потоки, ожидающие его (это "работает как задумано"), поэтому наивный способ использования эполла с пулом потоков бесполезен. По крайней мере, для TCP-сервера это не является большой проблемой, так как частичные запросы в любом случае придется сначала собирать, поэтому наивная многопоточная реализация не будет работать в любом случае.
- Работает не так, как можно было бы ожидать при чтении / записи файла ("всегда готов").
- Не может использоваться с AIO до недавнего времени, теперь возможно через
eventfd
, но требует (на сегодняшний день) недокументированной функции. - Если приведенные выше предположения не соответствуют действительности, epoll может быть неэффективным, и
poll
может работать одинаково или лучше. epoll
не может делать "магию", то есть все равно обязательно O(N) по отношению к числу событий, которые происходят.- Тем не мение,
epoll
хорошо играет с новымrecvmmsg
системный вызов, так как он возвращает несколько уведомлений о готовности одновременно (столько, сколько доступно, вплоть до того, что вы указали какmaxevents
). Это позволяет получать, например, 15 уведомлений EPOLLIN с одним системным вызовом на занятом сервере и читать соответствующие 15 сообщений с помощью второго системного вызова (снижение системных вызовов на 93%!). К сожалению, все операции на одномrecvmmsg
Вызов ссылается на один и тот же сокет, поэтому он в основном полезен для сервисов на основе UDP (для TCP должен быть своего родаrecvmmsmsg
syscall, который также принимает дескриптор сокета на элемент!). - Дескрипторы всегда должны быть неблокирующими, и нужно проверять
EAGAIN
даже при использованииepoll
потому что есть исключительные ситуации, когдаepoll
Готовность отчетов и последующее чтение (или запись) все равно будут блокироваться. Это также относится и кpoll
/select
на некоторых ядрах (хотя, по-видимому, это было исправлено). - При наивной реализации возможно замедление медленных отправителей. Когда вслепую читаю до
EAGAIN
возвращается после получения уведомления, можно бесконечно читать новые входящие данные от быстрого отправителя, при этом полностью истощая медленного отправителя (если данные продолжают поступать достаточно быстро, вы можете не увидетьEAGAIN
некоторое время!). Относится кpoll
/select
таким же образом. - Режим Edge-Triggered имеет некоторые причуды и неожиданное поведение в некоторых ситуациях, поскольку документация (как справочные страницы, так и TLPI) является расплывчатой ("вероятно", "следует", "может") и иногда вводит в заблуждение относительно его работы.
В документации говорится, что все потоки, ожидающие одного epoll, сигнализируются. В нем также говорится, что уведомление сообщает о том, произошла ли операция ввода-вывода с момента последнего вызоваepoll_wait
(или так как дескриптор был открыт, если не было никакого предыдущего вызова).
Истинное, наблюдаемое поведение в режиме запуска по фронту намного ближе к тому, что "пробуждает первый поток, вызвавшийepoll_wait
сигнализирует о том, что IO-активность произошла с тех пор,epoll_wait
или функция чтения / записи в дескрипторе, и после этого только снова сообщает о готовности следующему потоку, вызывающему или уже заблокированному вepoll_wait
, для любых операций, происходящих после того, как кто-либо вызвал функцию read (или write) для дескриптора ". Это тоже имеет смысл... это не совсем то, что предлагает документация.
kqueue
- BSD аналог
epoll
, различное использование, похожий эффект. - Также работает на Mac OS X
- По слухам, быстрее (я никогда не использовал его, поэтому не могу сказать, правда ли это).
- Регистрирует события и возвращает набор результатов в одном системном вызове.
- BSD аналог
- Порты завершения ввода-вывода
- Эполл для Windows, точнее эполл на стероидах.
- Работает без проблем со всем, что ожидает или каким-либо образом оповещается (сокеты, таймеры ожидания, файловые операции, потоки, процессы)
- Если Microsoft правильно поняла в Windows, это порты завершения:
- Работает без проблем из коробки с любым количеством потоков
- Не гремит стадо
- Пробуждает потоки один за другим в порядке LIFO
- Сохраняет кеш теплым и минимизирует переключение контекста
- Уважает количество процессоров на машине или поставляет нужное количество рабочих
- Позволяет приложению публиковать события, что обеспечивает очень простую, отказоустойчивую и эффективную реализацию параллельной рабочей очереди (в моей системе запланировано более 500000 задач в секунду).
- Незначительный недостаток: нелегко удалить дескрипторы файлов после добавления (необходимо закрыть и снова открыть).
Каркасы
libevent - версия 2.0 также поддерживает завершение портов под Windows.
ASIO - Если вы используете Boost в своем проекте, не смотрите дальше: у вас уже есть это как boost-asio.
Любые предложения для простых / базовых уроков?
Платформы, перечисленные выше, поставляются с обширной документацией. Документы Linux и MSDN подробно объясняют порты epoll и завершения.
Мини-учебник по использованию epoll:
int my_epoll = epoll_create(0); // argument is ignored nowadays
epoll_event e;
e.fd = some_socket_fd; // this can in fact be anything you like
epoll_ctl(my_epoll, EPOLL_CTL_ADD, some_socket_fd, &e);
...
epoll_event evt[10]; // or whatever number
for(...)
if((num = epoll_wait(my_epoll, evt, 10, -1)) > 0)
do_something();
Мини-учебник для портов завершения ввода-вывода (обратите внимание, дважды вызывая CreateIoCompletionPort с разными параметрами):
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0); // equals epoll_create
CreateIoCompletionPort(mySocketHandle, iocp, 0, 0); // equals epoll_ctl(EPOLL_CTL_ADD)
OVERLAPPED o;
for(...)
if(GetQueuedCompletionStatus(iocp, &number_bytes, &key, &o, INFINITE)) // equals epoll_wait()
do_something();
(Эти мини-посты пропускают все виды проверки ошибок, и, надеюсь, я не делал никаких опечаток, но по большей части они должны быть в порядке, чтобы дать вам некоторое представление.)
РЕДАКТИРОВАТЬ:
Обратите внимание, что порты завершения (Windows) концептуально работают как epoll (или kqueue). Они сигнализируют, как следует из их названия, завершение, а не готовность. То есть вы запускаете асинхронный запрос и забываете о нем, пока через некоторое время вам не сообщат, что он завершен (либо успешно, либо не очень успешно, а также есть исключительный случай "завершено немедленно").
С помощью epoll вы блокируете до тех пор, пока не получите уведомление о том, что либо "некоторые данные" (возможно, всего один байт) прибыли и доступны, либо имеется достаточно места в буфере, чтобы вы могли выполнить операцию записи без блокировки. Только тогда вы начнете реальную операцию, которая, как мы надеемся, не будет блокировать (кроме того, что вы ожидаете, строгой гарантии для этого нет - поэтому неплохо установить дескрипторы на неблокирование и проверить EAGAIN [EAGAIN and EWOULDBLOCK) для сокетов, потому что, радость, стандарт допускает два разных значения ошибок]).