Действительно ли неблокирующий ввод-вывод быстрее, чем многопоточный блокирующий ввод-вывод? Как?

119

Я поискал в Интернете некоторые технические подробности о блокировании ввода-вывода и неблокирующем вводе-выводе, и я нашел несколько человек, утверждающих, что неблокирующий ввод-вывод будет быстрее, чем блокирующий ввод-вывод. Например в этом документе .

Если я использую блокирующий ввод-вывод, то, конечно, поток, который в настоящее время заблокирован, не может делать ничего другого ... Потому что он заблокирован. Но как только поток начинает блокироваться, ОС может переключиться на другой поток и не переключаться обратно, пока для заблокированного потока не появится что-то делать. Итак, пока в системе есть другой поток, которому требуется ЦП и который не заблокирован, не должно быть больше времени простоя ЦП по сравнению с неблокирующим подходом, основанным на событиях, не так ли?

Помимо сокращения времени простоя ЦП, я вижу еще один вариант увеличения количества задач, которые компьютер может выполнять в заданный промежуток времени: уменьшение накладных расходов, связанных с переключением потоков. Но как это сделать? И достаточно ли велики накладные расходы, чтобы показать измеримые эффекты? Вот идея того, как я могу представить, как это работает:

  1. Чтобы загрузить содержимое файла, приложение делегирует эту задачу структуре ввода-вывода на основе событий, передавая функцию обратного вызова вместе с именем файла.
  2. Структура событий делегирует операционную систему, которая программирует контроллер DMA жесткого диска для записи файла непосредственно в память.
  3. Структура событий позволяет запускать дополнительный код.
  4. По завершении копирования диска в память контроллер DMA вызывает прерывание.
  5. Обработчик прерывания операционной системы уведомляет платформу ввода-вывода на основе событий о том, что файл полностью загружен в память. Как оно это делает? Используя сигнал ??
  6. Код, который в настоящее время выполняется в рамках среды ввода-вывода событий, завершается.
  7. Инфраструктура ввода-вывода на основе событий проверяет свою очередь, видит сообщение операционной системы с шага 5 и выполняет обратный вызов, полученный на шаге 1.

Вот как это работает? Если нет, то как это работает? Это означает, что система событий может работать без необходимости явно касаться стека (например, реальный планировщик, которому нужно будет сделать резервную копию стека и скопировать стек другого потока в память при переключении потоков)? Сколько времени это на самом деле экономит? Есть что-то еще?

янки
источник
5
Короткий ответ: это больше связано с накладными расходами, связанными с наличием потока на соединение. неблокирующий io позволяет избежать наличия потока на соединение.
Дэн Д.
10
Блокировка ввода-вывода обходится дорого в системе, где вы не можете создать столько потоков, сколько существует соединений. На JVM вы можете создать несколько тысяч потоков, но что, если у вас более 100 000 соединений? Таким образом, вы должны придерживаться асинхронного решения. Однако есть языки, в которых потоки не являются дорогостоящими (например, зеленые потоки), например, в Go / Erlang / Rust, где 100 000 потоков не проблема. Когда количество потоков может быть большим, я считаю, что блокировка ввода-вывода дает более быстрое время отклика. Но это то, что мне также следует спросить у экспертов, так ли это на самом деле.
OlliP 08
@OliverPlow, я тоже так думаю, потому что блокировка ввода-вывода обычно означает, что мы позволяем системе обрабатывать «параллельное управление» вместо того, чтобы делать это самим, используя очереди задач и тому подобное.
Pacerier
1
@DanD., А что, если накладные расходы на наличие потоков равны накладным расходам на выполнение неблокирующего ввода-вывода? (обычно верно в случае зеленых нитей)
Pacerier
«копирование стека» не происходит. У разных потоков свои стеки по разным адресам. У каждого потока есть собственный указатель стека вместе с другими регистрами. Переключение контекста сохраняет / восстанавливает только архитектурное состояние (включая все регистры), но не память. Между потоками одного процесса ядру даже не нужно изменять таблицы страниц.
Питер Кордес,

Ответы:

44

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

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

  1. Один поток на соединение (может блокировать ввод-вывод, но также может быть неблокирующим вводом-выводом).
    Каждый поток требует ресурсов памяти (а также памяти ядра!), Что является недостатком. И каждый дополнительный поток означает больше работы для планировщика.
  2. Один поток для всех подключений.
    Это снижает нагрузку на систему, потому что у нас меньше потоков. Но это также мешает вам использовать полную производительность вашего компьютера, потому что вы можете в конечном итоге довести один процессор до 100% и позволить всем остальным процессорам бездействовать.
  3. Несколько потоков, каждый из которых обрабатывает некоторые соединения.
    Это снижает нагрузку на систему из-за меньшего количества потоков. И он может использовать все доступные процессоры. В Windows этот подход поддерживается API пула потоков .

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

В Windows использование небуферизованного файлового ввода-вывода означает, что объем записи должен быть кратен размеру страницы. Я не тестировал это, но похоже, что это также может положительно повлиять на производительность записи для буферизованной синхронной и асинхронной записи.

Описанные вами шаги с 1 по 7 дают хорошее представление о том, как это работает. В Windows операционная система сообщит вам о завершении асинхронного ввода-вывода ( WriteFileсо OVERLAPPEDструктурой) с помощью события или обратного вызова. Функции обратного вызова будут вызываться только, например, когда ваш код вызывает WaitForMultipleObjectsExс bAlertableустановленным значением true.

Еще немного чтения в Интернете:

Вернер Хенце
источник
С точки зрения Интернета, общеизвестные данные (Интернет, комментарии экспертов) предполагают, что значительно увеличив макс. количество потоков запросов плохо влияет на блокировку ввода-вывода (делая обработку запросов еще медленнее) из-за увеличения памяти и времени переключения контекста, но разве Async IO не делает то же самое при переносе задания на другой поток? Да, теперь вы можете обслуживать больше запросов, но иметь такое же количество потоков в фоновом режиме ... в чем реальная выгода?
JavierJ
1
@JavierJ Похоже, вы верите, что если n потоков выполняют ввод-вывод асинхронного файла, другие n потоков будут созданы для выполнения ввода-вывода файла блокировки? Это неправда. ОС имеет поддержку ввода-вывода асинхронных файлов и не требует блокировки при ожидании завершения ввода-вывода. Он может ставить в очередь запросы ввода-вывода, и если происходит аппаратное прерывание (например, DMA), он может пометить запрос как выполненный и установить событие, которое сигнализирует потоку вызывающего абонента. Даже если потребуется дополнительный поток, ОС сможет использовать этот поток для нескольких запросов ввода-вывода из нескольких потоков.
Вернер Хенце,
Спасибо, имеет смысл задействовать поддержку ввода-вывода асинхронного файла ОС, но когда я пишу код для фактической реализации этого (с веб-точки зрения), скажем, с помощью Java Servlet 3.0 NIO, я все еще вижу поток для запроса и фоновый поток ( async) для чтения файла, базы данных или чего-то еще.
JavierJ
1
@piyushGoyal Перезаписываю свой ответ. Надеюсь, теперь стало понятнее.
Вернер Хенце,
1
В Windows использование асинхронного файлового ввода-вывода означает, что размер записи должен быть кратен размеру страницы. - нет, это не так. Вы думаете о небуферизованном вводе-выводе. (Их часто используют вместе, но это не обязательно.)
Гарри Джонстон
29

Ввод-вывод включает в себя несколько видов операций, таких как чтение и запись данных с жестких дисков, доступ к сетевым ресурсам, вызов веб-служб или получение данных из баз данных. В зависимости от платформы и типа операции асинхронный ввод-вывод обычно использует преимущества любого оборудования или поддержки системы низкого уровня для выполнения операции. Это означает, что это будет выполняться с минимальным влиянием на процессор.

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

Если операция ввода-вывода выполняется синхронно, она не дает своему работающему потоку ничего делать, пока операция не завершится. Среда выполнения не знает, когда операция ввода-вывода завершается, поэтому она будет периодически предоставлять некоторое время ЦП ожидающему потоку, время ЦП, которое в противном случае могло бы использоваться другими потоками, которые имеют фактические операции, связанные с ЦП.

Итак, как упоминалось в @ user1629468, асинхронный ввод-вывод не обеспечивает лучшую производительность, а, скорее, лучшую масштабируемость. Это очевидно при работе в контекстах с ограниченным числом доступных потоков, как в случае с веб-приложениями. Веб-приложение обычно использует пул потоков, из которого они назначают потоки каждому запросу. Если запросы блокируются при длительных операциях ввода-вывода, существует риск истощения веб-пула и замораживания веб-приложения или замедления ответа.

Я заметил одну вещь: асинхронный ввод-вывод - не лучший вариант при работе с очень быстрыми операциями ввода-вывода. В этом случае преимущество отсутствия занятости потока во время ожидания завершения операции ввода-вывода не очень важно, а тот факт, что операция запускается в одном потоке, а завершается в другом, добавляет накладные расходы к общему выполнению.

Вы можете прочитать более подробное исследование, которое я недавно провел по теме асинхронного ввода-вывода и многопоточности здесь .

Флорин Думитреску
источник
Интересно, стоит ли проводить различие между операциями ввода-вывода, которые, как ожидается, будут завершены, и вещами, которые могут не завершиться [например, «получить следующий символ, который поступает на последовательный порт», в случаях, когда удаленное устройство может или не может отправить что угодно]. Если ожидается, что операция ввода-вывода будет завершена в разумные сроки, можно отложить очистку связанных ресурсов до завершения операции. Однако, если операция может никогда не завершиться, такая задержка будет неоправданной.
supercat
@supercat сценарий, который вы описываете, используется в приложениях и библиотеках более низкого уровня. Серверы полагаются на него, поскольку они постоянно ждут входящих подключений. Асинхронный ввод-вывод, как описано выше, не может соответствовать этому сценарию, поскольку он основан на запуске определенной операции и регистрации обратного вызова для ее завершения. В случае, который вы описываете, вам необходимо зарегистрировать обратный вызов для системного события и обработать каждое уведомление. Вы постоянно обрабатываете ввод, а не выполняете операции. Как уже говорилось, это обычно делается на низком уровне, почти никогда в ваших приложениях.
Флорин Думитреску
Этот шаблон довольно распространен для приложений, которые поставляются с различным типом оборудования. Последовательные порты не так распространены, как раньше, но микросхемы USB, которые имитируют последовательные порты, довольно популярны при разработке специализированного оборудования. Символы из таких вещей обрабатываются на уровне приложения, так как ОС не будет иметь возможности узнать, что последовательность вводимых символов означает, например, что денежный ящик был открыт, и должно быть куда-то отправлено уведомление.
supercat
Я не думаю, что часть затрат ЦП на блокирование ввода-вывода является точной: в состоянии блокировки поток, инициировавший блокирующий ввод-вывод, находится в ожидании ОС и не требует затрат ЦП до тех пор, пока ввод-вывод не будет полностью завершен, только после этого возобновляет ли ОС (уведомляет прерываниями) заблокированный поток. То, что вы описали (занятое ожидание путем длительного опроса), - это не то, как блокирование ввода-вывода реализовано практически в любой среде выполнения / компиляторе.
Лифу Хуанг,
4

Основная причина использования AIO - масштабируемость. Если рассматривать в контексте нескольких потоков, преимущества не очевидны. Но когда система масштабируется до 1000 потоков, AIO будет предлагать гораздо лучшую производительность. Предостережение: библиотека AIO не должна создавать дополнительных узких мест.

fissurezone
источник
4

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

Первый сценарий часто называют «параллельным» программированием. Второй сценарий часто называют «параллельным» или «асинхронным» программированием, хотя «параллельный» иногда также используется для обозначения случая простого разрешения операционной системе чередовать выполнение нескольких задач, независимо от того, должно ли такое выполнение размещать последовательно или если для параллельного выполнения можно использовать несколько ресурсов. В этом последнем случае «параллельный» обычно относится к способу написания выполнения в программе, а не с точки зрения фактической одновременности выполнения задачи.

Обо всем этом очень легко говорить с неявными предположениями. Например, некоторые быстро заявляют, что «асинхронный ввод-вывод будет быстрее, чем многопоточный ввод-вывод». Это утверждение сомнительно по нескольким причинам. Во-первых, может случиться так, что некоторая данная структура асинхронного ввода-вывода реализована именно с многопоточностью, и в этом случае они являются одним и тем же, и не имеет смысла говорить, что одна концепция «быстрее, чем другая». ,

Во-вторых, даже в случае однопоточной реализации асинхронной инфраструктуры (например, однопоточного цикла событий) вы все равно должны делать предположения о том, что делает этот цикл. Например, одна глупая вещь, которую вы можете сделать с однопоточным циклом обработки событий, - это запросить его асинхронно выполнить две разные задачи, связанные исключительно с процессором. Если бы вы сделали это на машине только с идеализированным одноядерным процессором (игнорируя оптимизацию современного оборудования), то выполнение этой задачи «асинхронно» на самом деле не было бы иначе, чем выполнение ее с двумя независимо управляемыми потоками или с одним единственным процессом - - разница может сводиться к переключению контекста потока или оптимизации расписания операционной системы, но если обе задачи передаются процессору, это будет одинаково в любом случае.

Полезно представить себе множество необычных или глупых случаев, с которыми вы можете столкнуться.

«Асинхронный» не обязательно должен быть параллельным, например, как указано выше: вы «асинхронно» выполняете две задачи, связанные с ЦП, на машине с ровно одним процессорным ядром.

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

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

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

Вопросы, которые нужно задать: сколько вычислительных шагов мне нужно выполнить и сколько независимых систем ресурсов существует для их выполнения? Существуют ли подмножества вычислительных шагов, которые требуют использования независимых подкомпонентов системы и могут извлечь выгоду из одновременного использования? Сколько у меня процессорных ядер и каковы накладные расходы на использование нескольких процессоров или потоков для выполнения задач на отдельных ядрах?

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

Всякий раз, когда задачи привязаны к одному и тому же ресурсу (например, нескольким потребностям одновременного доступа к одной и той же сети или локальному ресурсу), многопоточность, вероятно, приведет к неудовлетворительным накладным расходам, а в то время как однопоточная асинхронность может привести к меньшим накладным расходам в таком ресурсе: ограниченная ситуация, она тоже не может привести к ускорению. В таком случае единственный вариант (если вы хотите ускорения) - сделать несколько копий этого ресурса доступными (например, несколько процессорных ядер, если дефицитным ресурсом является ЦП; лучшая база данных, которая поддерживает больше одновременных подключений, если ограниченный ресурс - база данных с ограничением количества соединений и т. д.)

Другими словами, разрешение операционной системе чередовать использование одного ресурса для двух задач не может быть быстрее, чем просто позволить одной задаче использовать ресурс, в то время как другая ждет, а затем позволить второй задаче завершиться последовательно. Кроме того, затраты планировщика на чередование означают, что в любой реальной ситуации это фактически вызывает замедление. Не имеет значения, происходит ли чередование использования ЦП, сетевого ресурса, ресурса памяти, периферийного устройства или любого другого системного ресурса.

Ely
источник
2

Одна из возможных реализаций неблокирующего ввода-вывода - это именно то, что вы сказали, с пулом фоновых потоков, которые блокируют ввод-вывод и уведомляют поток-инициатор ввода-вывода через некоторый механизм обратного вызова. Фактически, именно так работает модуль AIO в glibc. Вот некоторые расплывчатые подробности о реализации.

Хотя это хорошее решение, которое довольно переносимо (если у вас есть потоки), ОС обычно способна более эффективно обслуживать неблокирующий ввод-вывод. В этой статье в Википедии перечислены возможные реализации помимо пула потоков.

Miguel
источник
2

В настоящее время я нахожусь в процессе реализации async io на встроенной платформе с использованием протопотоков. Неблокирующий io делает разницу между работой со скоростью 16000 и 160 кадров в секунду. Самым большим преимуществом неблокирующего io является то, что вы можете структурировать свой код для выполнения других задач, в то время как оборудование делает свое дело. Параллельно можно производить даже инициализацию устройств.

Мартин

user2826084
источник
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 

Объяснение «Узел быстрее, потому что он не блокирует ...» - это немного маркетинговый ход, и это отличный вопрос. Он эффективен и масштабируем, но не совсем однопоточный.

SmokestackLightning
источник
0

Улучшение, насколько мне известно, заключается в том, что асинхронный ввод-вывод использует (я говорю о системе MS, просто для пояснения) так называемые порты завершения ввода-вывода . Используя асинхронный вызов, фреймворк автоматически использует такую ​​архитектуру, и это должно быть намного более эффективным, чем стандартный механизм потоковой передачи. По личному опыту могу сказать, что вы почувствуете, что ваше приложение более реактивно, если вы предпочитаете AsyncCalls вместо блокировки потоков.

Феличе Поллано
источник
0

Позвольте мне привести контрпример, что асинхронный ввод-вывод не работает. Я пишу прокси, похожий на приведенный ниже boost :: asio. https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp

Однако сценарий моего случая таков: входящие (со стороны клиента) сообщения быстрые, в то время как исходящие (на стороне сервера) медленные для одного сеанса, чтобы не отставать от входящей скорости или максимизировать общую пропускную способность прокси, мы должны использовать несколько сеансов под одним подключением.

Таким образом, эта структура асинхронного ввода-вывода больше не работает. Нам действительно нужен пул потоков для отправки на сервер, назначив каждому потоку сеанс.

Жидян Ду
источник