В чем разница между epoll, poll, threadpool? - PullRequest
74 голосов
/ 04 ноября 2010

Может кто-нибудь объяснить, в чем разница между epoll, poll и пулом потоков?

  • Какие плюсы / минусы?
  • Какие-либо предложения для фреймворков?
  • Какие-нибудь предложения для простых / базовых уроков?
  • Кажется, что epoll и poll относятся к Linux ... Есть ли эквивалентная альтернатива для Windows?

1 Ответ

211 голосов
/ 27 марта 2011

Пул потоков на самом деле не вписывается в ту же категорию, что и poll и epoll, поэтому я предполагаю, что вы ссылаетесь на пул потоков, как в «пуле потоков для обработки многих соединений с одним потоком на соединение».

Плюсы и минусы

  • threadpool
    • Разумно эффективен для малого и среднего параллелизма, может даже превосходить другие методы.
    • Использует несколько ядер.
    • Не масштабируется намного дальше «нескольких сотен», хотя некоторые системы (например, Linux) могут в принципе нормально планировать 100 000 потоков.
    • Наивная реализация демонстрирует « громоподобное стадо "проблема.
    • Помимо переключения контекста и громового стада, нужно учитывать память.Каждый поток имеет стек (как правило, не менее мегабайта).Поэтому тысячи потоков занимают гигабайт оперативной памяти только для стека.Даже если эта память не выделена, она все равно отнимает значительное адресное пространство в 32-битной ОС (на самом деле проблема не в 64-битной версии).
    • Потоки могут фактически использовать epoll,хотя очевидный способ (все потоки блокируются на epoll_wait) бесполезен, потому что epoll будет пробуждать каждый ожидающий поток, поэтому у него все равно будут возникать те же проблемы.
      • Оптимальное решение: один поток прослушивает epoll, выполняет мультиплексирование ввода и передает запросы в пул потоков.
      • futex здесь ваш друг в сочетании, например, с очереди быстрой пересылкиза нитку.Хотя futex плохо документировано и громоздко, он предлагает именно то, что нужно.epoll может возвращать несколько событий одновременно, а futex позволяет эффективно и точно контролировать пробуждение N заблокированных потоков одновременно (N в идеале min(num_cpu, num_events) в идеале) и в лучшем случаеВ этом случае он вообще не требует дополнительного переключения системного вызова / контекста.
      • Нетривиально реализовать, требует некоторой осторожности.
  • fork (он же пул старой моды)
    • Разумно эффективен для малого и среднего параллелизма.
    • Не масштабируется сверх «нескольких сотен».
    • Переключатели контекста стоят на намного дороже (разные адресные пространства!).
    • Значительно хуже масштабируется на старых системах, где fork намного дороже (полная копия всех страниц).Даже в современных системах fork не является «бесплатным», хотя накладные расходы в основном объединяются механизмом копирования при записи.В больших наборах данных, которые также модифицированы , значительное число ошибок страниц после fork может негативно повлиять на производительность.
    • Однако доказано, что они надежно работают более 30 лет.
    • Смехотворно прост в реализации и надежен: если какой-либо из процессов рухнет, мир не закончится.Нет (почти) ничего, что вы можете сделать неправильно.
    • Очень склонны к "гремящему стаду".
  • poll / select
    • Два варианта (BSD и System V) более или менее одинаковые.
    • Несколько старое и медленное, несколько неловкое использование, но практически нет платформы, которая их не поддерживает.
    • Ожидание, пока "что-то не случится" с набором дескрипторов
      • Позволяет одному потоку / процессу обрабатывать много запросов одновременно.
      • Нет многоядерного использования.
    • Необходимо копировать список дескрипторов из пользователя в пространство ядра каждый раз, когда вы ждете.Необходимо выполнить линейный поиск по дескрипторам.Это ограничивает его эффективность.
    • Не масштабируется до «тысяч» (на самом деле, жесткий предел около 1024 в большинстве систем или до 64 в некоторых).
    • Используйте его, потому что этопереносимый, если вы имеете дело только с дюжиной дескрипторов (без проблем с производительностью), или если вы должны поддерживать платформы, у которых нет ничего лучше.Не используйте иначе.
    • Концептуально сервер становится немного сложнее, чем разветвленный, поскольку теперь вам нужно поддерживать множество соединений и конечный автомат для каждого соединения, и вы должны мультиплексировать между запросами по мере их поступления, собирать частичные запросы и т. Д.Простой разветвленный сервер просто знает об одном сокете (ну, два, считая сокет прослушивания), читает, пока он не получит то, что он хочет, или пока соединение не будет наполовину закрыто, а затем записывает все, что он хочет.Он не беспокоится ни о блокировке, ни о готовности, ни о голодании, ни о каких-то не связанных данных, это проблема другого процесса.
  • epoll
    • Только для Linux.
    • Концепция дорогостоящих модификаций и эффективное ожидание:
      • Копирует информацию о дескрипторах в пространство ядра при добавлении дескрипторов (epoll_ctl)
        • Thisобычно это то, что происходит редко .
      • Требуется ли не копировать данные в пространство ядра при ожидании событий (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, который также принимает дескриптор сокета на элемент!).
      • Дескрипторы должны всегда быть установлены на неблокирование, и следует проверять наличие EAGAIN даже при использовании epoll, поскольку существуют исключительные ситуации, когда epoll сообщает о готовности и последующем чтении (или записи) будет все еще блок. Это также относится к poll / select в некоторых ядрах (хотя, по-видимому, это было исправлено).
      • С наивным реализацией возможно голодание медленных отправителей. При слепом чтении до возврата EAGAIN после получения уведомления можно бесконечно считывать новые входящие данные от быстрого отправителя, полностью истощая медленного отправителя (пока данные поступают достаточно быстро, вы можете не увидеть EAGAIN некоторое время!). Применяется к poll / select таким же образом.
      • Режим Edge-Triggered имеет некоторые странности и неожиданное поведение в некоторых ситуациях, поскольку документация (как справочные страницы, так и TLPI) является расплывчатой ​​(«вероятно», «следует», «может») и иногда вводит в заблуждение относительно его работы. 1208 * В документации говорится, что все потоки, ожидающие одного epoll, сигнализируются. В нем также говорится, что в уведомлении указывается, произошла ли операция ввода-вывода с момента последнего вызова epoll_wait (или с тех пор, как дескриптор был открыт, если не было предыдущего вызова).
        Истинное наблюдаемое поведение в режиме запуска по фронту намного ближе к тому, что «пробуждает первый поток, вызвавший epoll_wait, сигнализируя о том, что активность ввода-вывода произошла с любого последнего вызова или epoll_wait или функция чтения / записи в дескрипторе, и после этого только снова сообщает о готовности следующему потоку, вызывающему или уже заблокированному в epoll_wait, для любые операции, происходящие после любого , вызванного функцией чтения (или записи) дескриптора ". Это тоже имеет смысл ... просто не совсем то, что предлагает документация.
  • kqueue
    • BSD аналог epoll, различное использование, аналогичный эффект.
    • Также работает в Mac OS X
    • По слухам, быстрее (я никогда не использовал его, поэтому не могу сказать, правда ли это).
    • Регистрирует события и возвращает набор результатов в одном системном вызове.
  • Порты завершения ввода-вывода
    • Epoll для Windows, точнее epoll на стероидах.
    • Работает без проблем с всем , которое является ожидаемым или предупреждающим каким-либо образом (сокеты, ожидаемые таймеры, файловые операции, потоки, процессы)
    • Если Microsoft правильно поняла в Windows, это порты завершения:
      • Работает без проблем из коробки с любым количеством нитей
      • Не гремит стадо
      • Пробуждает потоки одну за другой в порядке LIFO
      • Сохраняет кэши в тепле и минимизирует переключение контекста
      • Уважает количество процессоров на машине или поставляет нужное количество рабочих
    • Позволяет приложению публиковать события, что обеспечивает очень простую, отказоустойчивую и эффективную реализацию параллельной рабочей очереди (в моей системе предусмотрено более 500 000 задач в секунду).
    • Незначительный недостаток: нелегко удалить дескрипторы файлов после добавления (необходимо закрыть и снова открыть).

рамочные

libevent - Версия 2.0 также поддерживает порты завершения в Windows.

ASIO - Если вы используете Boost в своем проекте, не смотрите дальше: у вас уже есть это как boost-asio.

Какие-нибудь предложения для простых / базовых уроков?

Платформы, перечисленные выше, поставляются с обширной документацией. Linux docs и 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 * 1309]* и EWOULDBLOCK для сокетов, потому что, о боже, стандарт допускает два разных значения ошибок]).

...