Яка різниця між epoll, poll, threadpool?


76

Може хто - небудь пояснити , в чому різниця між epoll, pollі ThreadPool?

  • Які плюси / мінуси?
  • Будь-які пропозиції щодо фреймворків?
  • Будь-які пропозиції щодо простих / базових підручників?
  • Здається, epollі pollспецифічні для Linux ... Чи існує еквівалентна альтернатива Windows?

Відповіді:


217

Threadpool насправді не входить до тієї ж категорії, що опитування та epoll, тому я припускаю, що ви маєте на увазі threadpool як у "threadpool для обробки багатьох з'єднань з одним потоком на з'єднання".

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

  • нитка пулу
    • Розумно ефективний для малого та середнього паралелізму, може навіть перевершити інші методи.
    • Використовує кілька ядер.
    • Масштаб не значно перевищує "кілька сотень", хоча деякі системи (наприклад, Linux) в принципі можуть запланувати 100 000 с потоків.
    • Наївна реалізація демонструє проблему " громового стада ".
    • Окрім переключення контексту та громового стада, слід враховувати пам’ять. Кожен потік має стек (зазвичай принаймні мегабайт). Тому тисяча потоків займає гігабайт оперативної пам'яті лише для стека. Навіть якщо ця пам'ять не зафіксована, вона все одно забирає значний адресний простір під 32-розрядною ОС (насправді проблема не перевищує 64 біт).
    • Потоки можуть насправді використовувати epoll, хоча очевидний спосіб (усі потоки блоковані epoll_wait) не приносить користі, оскільки epoll пробудить кожен потік, що чекає на нього, тому у нього все одно будуть ті самі проблеми.
      • Оптимальне рішення: одиночний потік прослуховує epoll, виконує мультиплексування вхідних даних і передає повні запити пулу потоків.
      • futexваш друг тут, у поєднанні з, наприклад, чергою перемотування вперед для кожного потоку. Хоча він погано задокументований і громіздкий, futexпропонує саме те, що потрібно. epollможе повертати кілька подій одночасно і futexдозволяє ефективно та точно контролювати спосіб одночасно пробуджувати N заблокованих потоків (N min(num_cpu, num_events)ідеально), а в найкращому випадку взагалі не передбачає додаткового перемикача syscall / context.
      • Не тривіальний у реалізації, дбає про певні дії.
  • fork (він же старовинний модний пул)
    • Розумно ефективний для малого та середнього паралелізму.
    • Масштаб не значно перевищує "кілька сотень".
    • Перемикачі контексту набагато дорожчі (різні адресні простори!).
    • Значно гірше масштабується у старих системах, де форк набагато дорожчий (глибока копія всіх сторінок). Навіть у сучасних системах forkце не "безкоштовно", хоча накладні витрати здебільшого об'єднуються механізмом копіювання і запису. На великих наборах даних, які також модифікуються , значна кількість помилок сторінок, що виникають після цього, forkможе негативно вплинути на продуктивність.
    • Однак доведено, що надійно працює протягом 30 років.
    • Смішно простий у реалізації та надійний: якщо будь-який із процесів зазнає аварії, світ не закінчується. Ви (майже) нічого не можете зробити неправильно.
    • Дуже схильний до "громового стада".
  • poll / select
    • Два смаки (BSD проти системи 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 повинен бути якийсь recvmmsmsgsyscall, який також бере дескриптор сокета на елемент!).
      • Дескриптори завжди повинні бути встановлені на неблокуючі, і слід перевіряти їх EAGAINнавіть при використанні, epollоскільки існують виняткові ситуації, коли epollготовність звітів та подальше читання (або запис) все одно блокуються. Це також відноситься і до poll/ selectна деяких ядрах (хоча це ймовірно було виправлено).
      • При наївній реалізації можливий голод повільних відправників. Якщо читати наосліп, доки не EAGAINбуде повернуто після отримання повідомлення, можна нескінченно читати нові вхідні дані від швидкого відправника, повністю голодуючи повільного відправника (якщо дані надходять досить швидко, ви можете не бачити їх EAGAINдосить довго! ). Застосовується до poll/ selectтаким же чином.
      • Режим, що запускається краєм, має деякі дивацтва та несподівану поведінку в деяких ситуаціях, оскільки документація (як сторінки користувача, так і TLPI) є розмитою ("ймовірно", "слід", "може") і іноді вводить в оману щодо своєї роботи.
        У документації зазначено, що всі сигнали, що очікують на одну епоху, сигналізуються. Далі йдеться про те, що сповіщення повідомляє, чи відбувалась активність вводу-виводу з моменту останнього виклику epoll_wait(або з моменту відкриття дескриптора, якщо попереднього виклику не було).
        Істинне, спостерігається поведінка в режимі краю спрацьовує набагато ближче до «будить перший потік , який закликав epoll_wait, сигналізуючи , що діяльність IO сталося з ким останнього виклику або epoll_wait абофункція читання / запису на дескрипторі, а потім лише знову повідомляє про готовність наступному потоку, що викликає або вже заблокований epoll_wait , для будь-яких операцій, що відбуваються після того, як хтось викликав функцію читання (або запису) на дескрипторі ". Це начебто має сенс теж ... це просто не зовсім те, що пропонується в документації.
  • kqueue
    • BSD аналог на epoll, різне використання, подібний ефект.
    • Також працює на Mac OS X
    • Подейкують, що це швидше (я ніколи цим не користувався, тому не можу сказати, чи це правда).
    • Реєструє події та повертає набір результатів в одній системі виклику.
  • Порти завершення введення-виводу
    • Epoll для Windows, точніше epoll на стероїдах.
    • Працює безперебійно з усім, що можна зачекати чи застерегти якимось чином (сокети, таймери очікування, файлові операції, потоки, процеси)
    • Якщо корпорація Майкрософт має одне право в Windows, це порти завершення:
      • Працює безтурботно з коробки з будь-якою кількістю ниток
      • Ні громового стада
      • Будить нитки по черзі в порядку LIFO
      • Зберігає кеші теплими та мінімізує перемикання контексту
      • Поважає кількість процесорів на машині або забезпечує бажану кількість робітників
    • Дозволяє програмі публікувати події, що забезпечує дуже просту, безпечну та ефективну реалізацію черги паралельної роботи (планує в моїй системі понад 500 000 завдань на секунду).
    • Незначний недолік: Нелегко видалити дескриптори файлів після додавання (їх потрібно закрити та повторно відкрити).

Рамки

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();

(Ці мініатюри опускають всі види перевірки помилок, і, сподіваюся, я не робив жодних помилок, але вони здебільшого повинні бути в порядку, щоб дати вам якусь ідею.)

EDIT:
Зверніть увагу, що порти завершення (Windows) концептуально працюють навпаки як epoll (або kqueue). Вони сигналізують, як випливає з назви, завершення , а не готовність . Тобто ви запускаєте асинхронний запит і забуваєте про нього, поки через деякий час вам не повідомлять, що він виконаний (або успішно, або не настільки успішно, і є винятковий випадок "негайно виконано" теж).
За допомогою epoll ви блокуєте, поки не отримаєте сповіщення про те, що «деякі дані» (можливо, лише один байт) надійшли і доступні, або є достатньо буферного простору, щоб ви могли робити операцію запису без блокування. Тільки тоді ви починаєте фактичну операцію, яка потім, сподіваємось, не заблокує (крім того, як ви очікували, для цього немає суворої гарантії - тому гарна ідея встановити дескриптори на неблокуючі та перевірити на EAGAIN [EAGAIN і EWOULDBLOCK для розеток, бо радість, стандарт допускає два різні значення помилок]).


1
Я не погоджуюся з вашим твердженням про те, що порти завершення вводу-виводу - це одна річ, яку MS зробила правильно. Радий, що ви помітили його відсталий дизайн у редагуванні!
Matt

Приємна відповідь (+1). Але ви мали min(num_cpu, num_events)на увазі в описі "futex"?
Немо

@Nemo: Ти, звичайно, маєш рацію, повинен бути min, а не max- я виправлю помилку. Дякую.
Деймон

1
Насправді я дещо змінив свій погляд на це. Після роботи з RDMA API IOCP більше відповідає цій моделі. Потенційно продуктивність краща. На практиці я не настільки впевнений. У будь-якому разі ... Я б не сказав, що це вже відстало, просто інакше, і набагато важче розвести голову.
Метт

Мені подобаються всі деталі, які ви надали. Я думаю, що EPOLLET все ще пробуджує всі нитки. fs / eventpoll.c: ep_send_events_proc () - це єдина функція, яка використовує цей прапор, і лише для того, щоб визначити, чи слід її вставляти назад у список готових.
Ant Manelope
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.