Какая разница между epoll, poll, threadpool?

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

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

Ответ 1

Threadpool не вписывается в ту же категорию, что и опрос и epoll, поэтому я предполагаю, что вы ссылаетесь на threadpool, как в "threadpool", чтобы обрабатывать множество соединений с одним потоком для каждого соединения ".

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

  • ThreadPool
    • Разумно эффективен для малых и средних concurrency, может даже превосходить другие методы.
    • Использует несколько ядер.
    • Не масштабируется далеко за пределы "несколько сотен", хотя некоторые системы (например, Linux) могут в принципе запланировать только 100 000 потоков.
    • Наивная реализация демонстрирует " громовое стадо".
    • Помимо переключения контекста и громового стада, нужно учитывать память. Каждый поток имеет стек (обычно не менее мегабайта). Поэтому тысячи потоков берут гигабайт ОЗУ только для стека. Даже если эта память не выполнена, она по-прежнему убирает значительное адресное пространство под 32-разрядной ОС (на самом деле это не проблема под 64 бит).
    • Темы могут фактически использовать epoll, хотя очевидный путь (все блоки потоков на epoll_wait) бесполезен, потому что epoll пробудит каждую нить, ожидающую его, поэтому у него все равно будут те же проблемы.
      • Оптимальное решение: один поток прослушивает epoll, выполняет входное мультиплексирование и передает запросы пользователя в пул потоков.
      • futex является вашим другом здесь, в сочетании с, например, быстрая перемотка очереди на поток. Несмотря на плохо документированные и громоздкие, futex предлагает именно то, что нужно. epoll может возвращать сразу несколько событий, а futex позволяет эффективно и точно контролировать пробуждение N заблокированных потоков за один раз (N является min(num_cpu, num_events) в идеале), и в лучшем случае это не предполагает extra syscall/context switch вообще.
      • Непросто реализовать, берет на себя осторожность.
  • fork (a.k.a. old fashion threadpool)
    • Разумно эффективен для малых и средних concurrency.
    • Не масштабируется далеко за пределы "несколько сотен".
    • Контекстные коммутаторы намного дороже (разные адресные пространства!).
    • Весы значительно хуже в старых системах, где fork намного дороже (глубокая копия всех страниц). Даже в современных системах fork не является "бесплатным", хотя накладные расходы в основном объединены механизмом копирования на запись. В больших наборах данных, которые также изменены, значительное количество ошибок страницы после fork может отрицательно сказаться на производительности.
    • Однако, он надежно работает более 30 лет.
    • Смешно легко внедрять и прокладывать твердые тела: если какой-либо из процессов выйдет из строя, мир не закончится. Существует (почти) ничего, что вы можете сделать неправильно.
    • Очень склонный к "громовому стаду".
  • poll/select
    • Два варианта (BSD vs. System V) более или менее одинаковые.
    • Несколько старое и медленное, несколько неудобное использование, но практически нет платформы, которая их не поддерживает.
    • Ожидает, что "что-то произойдет" в наборе дескрипторов
      • Позволяет одному потоку/процессу обрабатывать сразу несколько запросов.
      • Нет многоядерного использования.
    • Нужно скопировать список дескрипторов от пользователя к пространству ядра каждый раз, когда вы ждете. Требуется выполнить линейный поиск по дескрипторам. Это ограничивает его эффективность.
    • Не слишком хорошо масштабируется до "тысяч" (на самом деле, в большинстве систем с жестким ограничением около 1024) или меньше, чем на 64).
    • Используйте его, потому что он переносится, если вы имеете дело только с десятком дескрипторов (без проблем с производительностью) или если вы должны поддерживать платформы, которые не имеют ничего лучшего. Не используйте иначе.
    • Концептуально сервер становится немного сложнее, чем разветвленный, так как теперь вам нужно поддерживать множество соединений и конечный автомат для каждого соединения, и вы должны мультиплексировать между запросами по мере их поступления, собирать частичные запросы и т.д. Простой вильчатый сервер просто знает о единственном сокете (ну, два, считая прослушивающий сокет), читает, пока он не получит то, что он хочет, или пока соединение не будет закрыто, а затем напишет все, что захочет. Он не беспокоится о блокировании, готовности или голоде, а также о некоторых несвязанных данных, которые возникают в некоторых других проблемах процесса.
  • epoll
    • Только Linux.
    • Концепция дорогостоящих изменений и эффективных ожиданий:
      • Копирует информацию о дескрипторах в пространство ядра при добавлении дескрипторов (epoll_ctl)
        • Обычно это происходит редко.
      • Не нужно копировать данные в пространство ядра при ожидании событий (epoll_wait)
        • Обычно это происходит очень часто.
      • Добавляет официанта (а точнее его структуру epoll) в очереди ожидания дескрипторов
        • Таким образом, дескриптор знает, кто слушает и напрямую сигнализирует официантам, когда это подходит, а не официанты, которые ищут список дескрипторов.
        • Противоположный способ работы poll
        • O (1) с малым k (очень быстрым) по числу дескрипторов вместо O (n)
    • Хорошо работает с timerfd и eventfd (потрясающее разрешение и точность таймера).
    • Хорошо работает с signalfd, устраняя неудобную обработку сигналов, делая их частью обычного потока управления очень элегантно.
    • Экземпляр epoll может рекурсивно содержать другие экземпляры epoll
    • Предположения, сделанные этой моделью программирования:
      • Большинство дескрипторов в большинстве случаев простаивают, мало всего (например, "полученные данные", "соединение закрыто" ) фактически происходят на нескольких дескрипторах.
      • В большинстве случаев вы не хотите добавлять/удалять дескрипторы из набора.
      • В большинстве случаев вы ожидаете, что что-то произойдет.
    • Некоторые незначительные ловушки:
      • Эпох, вызванный уровнем, пробуждает все ожидающие его потоки (это "работает по назначению" ), поэтому наивный способ использования epoll с threadpool бесполезен. По крайней мере, для сервера TCP это не большая проблема, так как частичные запросы должны быть собраны во-первых, так что наивная многопоточная реализация не будет работать в любом случае.
      • Не работает так, как можно было бы ожидать при чтении/записи файлов ( "всегда готово" ).
      • Невозможно использовать с AIO до недавнего времени, теперь можно с помощью eventfd, но для него требуется (до даты) недокументированная функция.
      • Если приведенные выше допущения неверны, epoll может быть неэффективным, а poll может работать одинаково или лучше.
      • epoll не может делать "волшебство", то есть все равно обязательно O (N) в отношении количества событий, которые происходят.
      • Тем не менее, epoll хорошо сочетается с новым syscall recvmmsg, так как он возвращает несколько уведомлений о готовности за один раз (столько, сколько доступно, до того, что вы указываете как maxevents). Это позволяет получать, например, 15 уведомлений EPOLLIN с одним системным вызовом на занятом сервере и прочитайте соответствующие 15 сообщений со вторым системным вызовом (сокращение системных вызовов на 93%!). К несчастью, все операции с одним вызовом recvmmsg относятся к одному и тому же сокету, поэтому он в основном полезен для служб на основе UDP (для TCP должен быть вид syscall recvmmsmsg, который также принимает дескриптор сокета для каждого элемента!).
      • Дескрипторы всегда должны быть настроены на неблокирование, и нужно проверить EAGAIN даже при использовании epoll, потому что есть исключительные ситуации, когда epoll сообщает о готовности, а последующее чтение (или запись) все равно будет блокироваться. Это также имеет место для poll/select на некоторых ядрах (хотя предположительно было исправлено).
      • С наивной реализацией возможно голодание медленных отправителей. Когда слепое чтение до тех пор, пока EAGAIN не будет возвращено после получения уведомления, можно бесконечно читать новые входящие данные от быстрого отправителя, в то время как он полностью голодает медленным отправителем (пока данные продолжают поступать достаточно быстро, вы можете не видеть EAGAIN какое-то время!). Аналогично применяется к poll/select.
      • В некоторых ситуациях режим Edge-triggered имеет некоторые причуды и неожиданное поведение, поскольку документация (как справочные страницы, так и TLPI) является неопределенной ( "вероятно", "должна", "может" ) и иногда вводит в заблуждение относительно ее работы. < ш > В документации указано, что все потоки, ожидающие одного epoll, сигнализируются. Далее указано, что уведомление сообщает вам, произошло ли с момента последнего вызова epoll_wait (или с момента открытия дескриптора, если предыдущий вызов не был). Истинное наблюдаемое поведение в режиме с граничным режимом намного ближе к "пробуждает первый поток, который вызвал epoll_wait, сигнализируя о том, что активность IO произошла с тех пор, как кто-либо в последний раз вызывал либо epoll_wait, либо функцию чтения/записи в дескрипторе, и после этого только готовность отчетов снова к следующему потоку, вызывающему или уже заблокированному в epoll_wait, для любых операций, которые происходят после того, как кто-либо вызвал функцию чтения (или записи) в дескрипторе". Это тоже имеет смысл... это не совсем то, что предлагает документация.
  • kqueue
    • BSD analogon to 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();

(Эти мини-туты опускают все виды проверки ошибок, и, надеюсь, я не делал никаких опечаток, но они должны по большей части быть в порядке, чтобы дать вам некоторую идею.)

EDIT:
Обратите внимание, что порты завершения (Windows) концептуально работают иначе, как epoll (или kqueue). Они сигнализируют, как их называют, завершение, а не готовность. То есть вы запускаете асинхронный запрос и забываете об этом, пока через некоторое время вам не скажут, что он завершил (либо успешно, либо не так успешно, и есть исключительный случай "завершен сразу" ).
С epoll вы блокируете, пока не получите уведомление о том, что либо "некоторые данные" (возможно, всего один байт) прибыл, либо доступен, либо имеется достаточное пространство для буфера, поэтому вы можете выполнять операцию записи без блокировки. Только тогда вы начнете фактическую операцию, которая, как мы надеемся, не будет блокирована (за исключением вас, как и следовало ожидать, для этого нет строгой гарантии - поэтому рекомендуется установить дескрипторы для неблокирования и проверить EAGAIN [EAGAIN и EWOULDBLOCK для сокетов, поскольку oh joy, стандарт допускает два разных значения ошибки]).