Чи не блокуючи введення / виведення дійсно швидше, ніж багатопотокове блокування вводу / виводу? Як?


118

Я шукав в Інтернеті деякі технічні деталі щодо блокування вводу-виводу та неблокування вводу-виводу, і знайшов декількох людей, які заявили, що незаблокування вводу-виводу буде швидше, ніж блокування вводу-виводу. Наприклад, у цьому документі .

Якщо я використовую блокуюче введення / виведення, то, звичайно, нитка, яка зараз заблокована, не може нічого іншого робити ... Тому що вона заблокована. Але як тільки нитка почне блокуватися, ОС може переключитися на інший потік і не переключитися назад, поки не буде що робити для заблокованого потоку. Отже, поки в системі є ще один потік, який потребує процесора і не блокований, не повинно бути більше часу простою процесора в порівнянні з неблокувальним підходом, заснованим на подіях, чи є?

Окрім скорочення часу роботи процесора в режимі очікування, я бачу ще один варіант збільшення кількості завдань, які комп’ютер може виконувати за певний часовий період: Скорочення накладних витрат, перетворених комутацією потоків. Але як це можна зробити? І чи накладні покриття досить великі, щоб показати вимірювані ефекти? Ось ідея про те, як я можу уявити, як це працює:

  1. Для завантаження вмісту файлу програма делегує це завдання рамці вводу-виводу на основі подій, передаючи функцію зворотного дзвінка разом з ім'ям файлу
  2. Рамка подій делегується операційній системі, яка програмує DMA-контролер жорсткого диска, щоб записати файл безпосередньо в пам'ять
  3. Рамка подій дозволяє виконувати подальший код.
  4. Після завершення копії диска на пам'ять контролер DMA викликає перерву.
  5. Обробник переривань операційної системи повідомляє основу подій введення-виводу про те, що файл повністю завантажений в пам'ять. Як це робити? Використовуючи сигнал ??
  6. Код, який зараз виконується в рамках події введення / виведення, завершується.
  7. Структура вводу-виводу на основі подій перевіряє свою чергу і бачить повідомлення операційної системи з кроку 5 та виконує зворотний виклик, отриманий на кроці 1.

Так це працює? Якщо цього немає, як це працює? Це означає, що система подій може працювати, не маючи потреби чітко торкатися стека (наприклад, реального планувальника, який потребував би резервного копіювання стека та копіювання стека іншого потоку в пам'ять під час перемикання потоків)? Скільки часу це насправді економить? Чи є ще це?


5
коротка відповідь: мова йде більше про накладні витрати потоку на з'єднання. неблокуючий Іо дозволяє уникати потоку на з'єднання.
Дан Д.

10
Блокування IO є дорогим у системі, де ви не можете створити стільки потоків, скільки є з'єднань. На JVM ви можете створити кілька тисяч потоків, але що робити, якщо у вас більше 100 000 з'єднань? Тому вам доведеться дотримуватися асинхронного рішення. Однак є мови, де нитки не дорогі (наприклад, зелені нитки), як у Go / Erlang / Rust, де не проблема мати 100 000 ниток. Коли кількість потоків може бути великою, я вважаю, що блокування IO дає швидший час відгуку. Але це теж я повинен запитати у експертів, чи це реально в дійсності.
OlliP

@OliverPlow, я теж думаю, тому що блокування IO зазвичай означає, що ми дозволяємо системі обробляти "паралельне управління", а не робити це самостійно, використовуючи черги завдань тощо.
Pacerier

1
@DanD., А що робити, якщо накладні витрати ниток дорівнюють накладним витратам, що виконуються, не блокуючи IO? (зазвичай це стосується зелених ниток)
Pacerier

"копіювання стека" не відбувається. Різні потоки мають свої стеки за різними адресами. Кожен потік має свій власний покажчик стека разом з іншими регістрами. Контекстний перемикач зберігає / відновлює лише архітектурний стан (включаючи всі регістри), але не пам'ять. Між потоками в одному і тому ж процесі ядра навіть не повинні змінювати таблиці сторінок.
Пітер Кордес

Відповіді:


44

Найбільшою перевагою неблокуючого або асинхронного вводу / виводу є те, що ваша нитка може продовжувати свою роботу паралельно. Звичайно, ви можете досягти цього також за допомогою додаткової нитки. Як ви заявляли для найкращої загальної (системної) продуктивності, я думаю, було б краще використовувати асинхронний введення / виведення, а не декілька потоків (так зменшуючи комутацію потоків).

Давайте розглянемо можливі реалізації мережевої серверної програми, яка буде обробляти 1000 клієнтів, підключених паралельно:

  1. Один потік на з'єднання (може блокувати введення-виведення, але також може бути неблокуючим введення-виведення).
    Кожен потік вимагає ресурсів пам'яті (також пам'ять ядра!), Що є недоліком. І кожен додатковий потік означає більше роботи для планувальника.
  2. Одна нитка для всіх з'єднань.
    Це приймає навантаження з системи, оскільки у нас менше ниток. Але це також заважає використовувати повну продуктивність вашої машини, тому що ви можете в кінцевому підсумку запустити один процесор на 100%, а всі інші процесори простоювати.
  3. Кілька ниток, де кожна нитка обробляє деякі з'єднання.
    Це сприймає навантаження з системи, оскільки є менше потоків. І він може використовувати всі доступні процесори. У Windows такий підхід підтримується API Thread Pool .

Звичайно, наявність більшої кількості потоків сама по собі не є проблемою. Як ви могли визнати, я вибрав досить велику кількість з'єднань / потоків. Я сумніваюся, що ви побачите будь-яку різницю між трьома можливими реалізаціями, якщо ми говоримо лише про десяток потоків (це також те, що пропонує Реймонд Чен у дописі про блог MSDN. Чи має обмеження у Windows 2000 потоків на процес? ).

У Windows, що використовує незаблокований файл, введення-виведення означає, що запис повинен бути розміром, кратним розміру сторінки. Я не перевіряв це, але здається, що це також може позитивно впливати на продуктивність запису для буферних синхронних та асинхронних записів.

Етапи з 1 по 7, які ви описуєте, дають гарне уявлення про те, як це працює. У Windows операційна система повідомить вас про завершення асинхронного вводу / виводу ( WriteFileзі OVERLAPPEDструктурою) за допомогою події або зворотного дзвінка. Функції зворотного виклику буде викликатися тільки для прикладу , коли ваш код викликів WaitForMultipleObjectsExз bAlertableвстановленим на true.

Ще кілька читання в Інтернеті:


З веб-точки зору загальновідомі знання (Інтернет, коментарі експертів) дозволяють припустити, що значно збільшити макс. кількість потоків запитів - це погана річ у блокуванні IO (що робить обробку запитів ще повільнішою) через збільшення пам’яті та час перемикання контексту, але хіба Async IO робить те ж саме, відкладаючи завдання на інший потік? Так, ви можете подавати більше запитів зараз, але мати однакову кількість потоків у фоновому режимі. Яка реальна користь від цього?
JavierJ

1
@JavierJ Ви, здається, вірите, що якщо n ниток не асинхронізує файл IO, буде створено ще n ниток, щоб зробити блокування IO файлів? Це не правда. В ОС є підтримка вводу-виводу файлу асинхронізації, і її не потрібно блокувати, коли чекати завершення IO. Він може встановити чергу на запити вводу-виводу, і якщо трапляється переривання апаратного забезпечення (наприклад, DMA), він може позначити запит як виконаний та встановити подію, яка сигналізує про потік абонентів. Навіть якщо потрібен додатковий потік, ОС змогла б використовувати цей потік для декількох запитів вводу-виводу з декількох потоків.
Вернер Гензе

Дякую, має сенс залучати підтримку IO файлу OS async, але коли я пишу код для реальної реалізації цього (з точки зору веб-сторінки), скажімо, з Java Servlet 3.0 NIO я все ще бачу потік запиту та фонову нитку ( async) циклічно читати файл, базу даних чи будь-що інше.
JavierJ

1
@piyushGoyal Я переписав свою відповідь. Сподіваюся, зараз зрозуміліше.
Вернер Гензе

1
У Windows з використанням асинхронного файлу введення / виведення означає, що запис повинен бути розміром, кратним розміру сторінки. - ні, це не так. Ви думаєте про нерозподілене введення / виведення. (Їх часто використовують разом, але вони не повинні бути.)
Гаррі Джонстон,

29

Введення / виведення включає в себе різні види операцій, такі як читання та запис даних з жорстких дисків, доступ до мережевих ресурсів, виклик веб-служб або отримання даних з баз даних. Залежно від платформи та типу операції, асинхронний введення / вивід зазвичай використовує будь-яке апаратне забезпечення або системну підтримку низького рівня для виконання операції. Це означає, що вона буде виконуватися з якомога меншим впливом на процесор.

На рівні програми, асинхронний введення / вивід запобігає потокам від необхідності чекати завершення операцій вводу / виводу. Як тільки запускається асинхронна операція вводу / виводу, вона звільняє потік, на якому вона була запущена, і реєструється зворотний виклик. Коли операція завершена, зворотний виклик ставиться в чергу для виконання в першому доступному потоці.

Якщо операція вводу / виводу виконується синхронно, вона продовжує робити її поточну нитку, не роблячи нічого, поки операція не завершиться. Час виконання не знає, коли завершується операція вводу / виводу, тому він періодично надаватиме деякий час процесора потоку очікування, час процесора, який інакше міг би бути використаний іншими потоками, які мають виконувати фактичні операції, пов'язані з процесором.

Отже, як згадував @ user1629468, асинхронний введення-вивід не забезпечує кращу продуктивність, а кращу масштабованість. Це очевидно при запуску в контекстах, які мають обмежену кількість потоків, як це стосується веб-додатків. Веб-програма зазвичай використовує пул потоків, з якого вони присвоюють потоки кожному запиту. Якщо запити блокуються при тривалих операціях вводу / виводу, існує ризик виснаження веб-пулу і змушення веб-програми заморозити або повільно реагувати.

Одне, що я помітив, - це те, що асинхронний введення / вивід - це не найкращий варіант, коли стосуються дуже швидких операцій вводу / виводу. У цьому випадку користь від того, щоб не тримати потік зайнятим під час очікування завершення операції вводу / виводу, не дуже важлива, а той факт, що операція запускається на одній нитці, а вона завершується на іншій, додає накладних витрат до загального виконання.

Ви можете прочитати більш детальне дослідження, яке я нещодавно робив на тему асинхронного вводу / виводу проти багатопотокового читання тут .


Цікаво, чи варто було б розрізняти операції вводу / виводу, які, як очікується, завершаться, і речі, які не можуть [наприклад "отримати наступний символ, який надходить на послідовний порт", у випадках, коли віддалений пристрій може чи не може надіслати що-небудь]. Якщо очікується, що операція вводу / виводу завершиться протягом розумного часу, можна затримати очищення відповідних ресурсів до завершення операції. Якщо операція ніколи не може завершитися, така затримка була б необґрунтованою.
supercat

@supercat сценарій, який ви описуєте, використовується в додатках і бібліотеках нижчого рівня. Сервери покладаються на нього, оскільки вони постійно чекають вхідних з'єднань. Асинхронний введення / вивід, як описано вище, не може вписатися в цей сценарій, оскільки він заснований на запуску певної операції та реєстрації зворотного дзвінка для його завершення. У випадку, який ви описуєте, вам потрібно зареєструвати зворотний виклик на системній події та обробити кожне повідомлення. Ви постійно обробляєте дані, а не виконуєте операції. Як було сказано, це зазвичай робиться на низькому рівні, майже ніколи у ваших додатках.
Флорін Думітреску

Ця модель є досить поширеною для програм, які постачаються з різними видами обладнання. Послідовні порти не такі поширені, як раніше, але USB-мікросхеми, що імітують послідовні порти, досить популярні в розробці спеціалізованого обладнання. Символи з таких речей обробляються на рівні додатків, оскільки ОС не зможе знати, що послідовність символів введення означає, наприклад, відкрили ящик грошових коштів і десь надіслати повідомлення.
supercat

Я не вважаю, що частина щодо вартості процесора блокування IO є точною: коли в стані блокування, нитка, що викликала блокування IO, ставиться на очікування ОС і не коштує періодів процесора до повного завершення IO, лише після цього чи ОС (повідомляє про переривання) поновлює заблокований потік. Те, що ви описали (зайняте очікування довгим опитуванням), - це не те, як блокування IO реалізується майже в будь-якому режимі виконання / компіляторі.
Ліфу Хуан

4

Основна причина використання AIO - це масштабованість. Якщо дивитися в контексті кількох ниток, переваги не очевидні. Але коли система масштабується до 1000 ниток, AIO запропонує набагато кращі показники. Застереження полягає в тому, що бібліотека AIO не повинна впроваджувати нові вузькі місця.


4

Щоб припустити підвищення швидкості за рахунок будь-якої форми багатокомп'ютерних обчислень, ви повинні припустити, що кілька задач на основі процесора виконуються одночасно на кількох обчислювальних ресурсах (як правило, ядра процесора), або ж, що не всі завдання покладаються на одночасне використання той самий ресурс - тобто деякі завдання можуть залежати від одного системного підкомпонента (дискове зберігання, скажімо), тоді як деякі завдання залежать від іншого (отримання зв'язку з периферійного пристрою), а інші можуть вимагати використання процесорних ядер.

Перший сценарій часто називають "паралельним" програмуванням. Другий сценарій часто називають "одночасним" або "асинхронним" програмуванням, хоча "паралельний" іноді також використовується для позначення випадку просто дозволу операційній системі переплутати виконання декількох завдань, незалежно від того, чи потрібно таке виконання розміщуйте серійно або якщо для досягнення паралельного виконання можуть використовуватися кілька ресурсів. В цьому останньому випадку "паралельний", як правило, відноситься до того, що виконання записується в програмі, а не з точки зору фактичної одночасності виконання завдання.

Про все це дуже просто говорити з негласними припущеннями. Наприклад, деякі швидко висувають претензію на зразок "Асинхронний введення / виведення буде швидше, ніж багатопотоковий введення / виведення". Ця претензія сумнівна з кількох причин. По-перше, може статися так, що деякі задані асинхронні рамки вводу / виводу реалізуються саме з багатопотоковою резьбою, і в цьому випадку вони однакові, і немає сенсу говорити, що одна концепція "швидше, ніж" інша .

По-друге, навіть у випадку, коли є однопоточна реалізація асинхронного фреймворку (наприклад, однонитковий цикл подій), ви все одно повинні зробити припущення про те, що робить цей цикл. Наприклад, одна нерозумна річ, яку ви можете зробити з однопоточним циклом подій, - це запит, щоб вона асинхронно виконала дві різні, суто пов'язані з процесором завдання. Якщо ви зробили це на машині з лише ідеалізованим ядром одного процесора (ігноруючи сучасні аппаратні оптимізації), то виконання цього завдання "асинхронно" насправді не було б інакше, ніж виконувати його з двома незалежно керованими потоками, або лише з одним самотнім процесом - - різниця може звестись до переключення контексту потоку або оптимізації розкладу операційної системи, але якщо обидва завдання надходять до центрального процесора, це було б у будь-якому випадку схожим.

Корисно уявити безліч незвичайних або дурних кутових справ, в які ви можете зіткнутися.

"Асинхронний" не повинен бути паралельним, наприклад, так само, як вище: ви "асинхронно" виконуєте два завдання, пов'язані з процесором, на машині з точно одним процесорним ядром.

Багатопотокове виконання не повинно бути одночасним: ви породжуєте два потоки на машині з одним ядром процесора або просите два потоки придбати будь-який інший дефіцитний ресурс (уявімо, скажімо, мережеву базу даних, яка може встановити лише один з'єднання за раз). Виконання потоків може бути переплетено, проте планувальник операційної системи вважає за потрібне, але їх загальний час виконання не може бути зменшений (і буде збільшено за допомогою перемикання контексту потоку) на одному ядрі (або в більш загальному випадку, якщо ви породжуєте більше потоків, ніж є ядра для їх запуску або мають більше потоків із запитом про ресурс, ніж те, що ресурс може підтримувати). Це ж стосується і багатопроцесорної обробки.

Таким чином, ні асинхронний введення / виведення, ні багатопотокова передача не повинні пропонувати будь-яке підвищення продуктивності з точки зору часу виконання. Вони навіть можуть уповільнити справи.

Якщо ви визначите конкретний випадок використання, як-от конкретна програма, яка одночасно здійснює мережевий виклик для отримання даних із підключеного до мережі ресурсу, наприклад віддаленої бази даних, а також виконує деякі локальні обчислення, пов'язані з процесором, тоді ви можете почати міркувати про відмінності в продуктивності між двома методами дають певне припущення про обладнання.

Запитання, які слід задати: Скільки обчислювальних кроків мені потрібно виконати і скільки незалежних систем ресурсів існує для їх виконання? Чи є підмножини обчислювальних кроків, які вимагають використання незалежних підкомпонентів системи і можуть отримати користь від цього одночасно? Скільки ядер процесора у мене є і яка накладні витрати на використання декількох процесорів або потоків для виконання завдань на окремих ядрах?

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

Кожного разу, коли завдання пов'язані одним і тим же ресурсом (наприклад, кілька потреб для одночасного доступу до однієї мережі або локального ресурсу), то багатопотокова передача, ймовірно, введе незадовільні накладні витрати, і в той час як однопотокова асинхронія може вводити менше накладних витрат, обмежена ситуація теж не може призвести до прискорення. У такому випадку єдиний варіант (якщо ви хочете прискорити роботу) - це зробити доступними кілька копій цього ресурсу (наприклад, декілька ядер процесора, якщо дефіцитним ресурсом є процесор; краща база даних, яка підтримує більше одночасних з'єднань, якщо дефіцитний ресурс це база даних з обмеженим з'єднанням тощо).

Інший спосіб зробити це: дозволити операційній системі переплутати використання одного ресурсу для двох завдань не може бути швидше, ніж просто дозволити одній задачі використовувати ресурс, а інша чекати, а потім дозволити другій задачі послідовно закінчуватися. Крім того, вартість планувальника переплетення означає в будь-якій реальній ситуації, що насправді створює уповільнення. Не має значення, чи відбувається перемежоване використання процесора, мережевого ресурсу, ресурсу пам'яті, периферійного пристрою чи будь-якого іншого системного ресурсу.


2

Можлива реалізація неблокуючого вводу-виводу - це саме те, що ви сказали, з пулом фонових потоків, які блокують введення-виведення та повідомляють потік джерела вводу-виводу через якийсь механізм зворотного виклику. Насправді так працює модуль AIO в glibc. Ось кілька неясних подробиць щодо реалізації.

Хоча це хороше рішення, яке є досить портативним (якщо у вас є потоки), ОС, як правило, може більш ефективно обслуговувати не блокуючи введення / виведення. Ця стаття у Вікіпедії перелічує можливі реалізації, крім пулу потоків.


2

Наразі я перебуваю в процесі впровадження async io на вбудованій платформі за допомогою прототрап. Неблокуючий іо робить різницю між бігом зі швидкістю 16000fps та 160fps. Найбільшою перевагою незаблокування io є те, що ви можете структурувати свій код для того, щоб робити інші речі, тоді як апаратне забезпечення робить це. Навіть ініціалізація пристроїв може здійснюватися паралельно.

Мартін


1

У Node запускаються кілька потоків, але це шар вниз під час виконання C ++.

"Так, так, NodeJS є однопотоковою, але це наполовину істина, насправді це керована подіями та однопотокова з фоновими робітниками. Основний цикл подій є однопоточним, але більшість робіт вводу / виводу працює на окремих потоках, оскільки API вводу / виводу в Node.js є асинхронними / не блокуючими за дизайном, щоб вмістити цикл подій. "

https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea

"Node.js не блокує, що означає, що всі функції (зворотні виклики) делеговані циклу подій, і вони виконуються (або можуть бути) виконані різними потоками. Цим обробляється час роботи Node.js."

https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98 

"Вузол швидше, тому що він не блокує ..." Пояснення - це трохи маркетинг, і це чудове запитання. Це ефективне та масштабоване, але не зовсім однопоточне.


0

Поліпшення, наскільки я знаю, полягає в тому, що асинхронний ввід / вивід використовує (я говорю про MS System, просто для уточнення) так звані порти завершення вводу / виводу . Використовуючи асинхронний виклик, фреймворк автоматично використовує таку архітектуру, і це, як передбачається, буде набагато ефективнішим від стандартного механізму різьблення. Як особистий досвід, я можу сказати, що ви відчутно відчуєте, що ваша програма більш реактивна, якщо ви віддаєте перевагу AsyncCalls замість блокування потоків.


0

Дозвольте надати вам контрприклад, який не працює асинхронним вводу / виводу. Я пишу проксі, схожий на нижче, використовуючи boost :: asio. https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp

Однак сценарій мого випадку полягає в тому, що вхідні повідомлення (з боку клієнтів) повідомлення швидкі, а вихідні (на сторону сервера) повільні протягом одного сеансу, щоб не відставати від швидкості вхідних даних або щоб максимально збільшити пропускну здатність проксі, ми повинні використовувати кілька сеансів під одним з'єднанням.

Таким чином, ця система асинхронного вводу / виводу вже не працює. Нам потрібен пул потоків для надсилання на сервер, присвоєння кожному потоку сеансу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.