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

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

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

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

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

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

Ответ 1

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

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

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

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

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

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

Более подробное чтение в Интернете:

Ответ 2

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

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

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

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

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

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

Ответ 3

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

Ответ 4

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 5

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

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

Ответ 6

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

Martin

Ответ 7

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

Ответ 8

В 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

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