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


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

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

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

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

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

7 96

7 ответов:

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

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

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

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

на Windows с помощью unbuffered file I / O означает, что записи должны иметь размер, кратный размеру страницы. У меня есть не проверял, но похоже, что это также может положительно повлиять на производительность записи для буферизованных синхронных и асинхронных операций записи.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Мартин

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