Асинхронный ввод-вывод

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

Входные и выходные (I/O) операции на компьютере могут быть весьма медленными, по сравнению с обработкой данных. Устройство ввода/вывода может быть на несколько порядков медленнее, чем оперативная память. Например, во время дисковой операции, которой требуется десять миллисекунд для выполнения, процессор, который работает на частоте один гигагерц, может выполнить десять миллионов циклов команд обработки.

Описание править

Виды ввода-вывода и примеры функций ввода-вывода Unix:

Блокирующий Неблокирующий
Синхронный write, read write, read + poll / select
Асинхронный - aio_write, aio_read

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

Так в чём же отличие асинхронного от синхронного подхода к неблокирующему вводу-выводу? Во втором случае блокировка избегается проверкой на наличие входящих данных или возможность записи исходящих данных. В асинхронном подходе проверка не требуется. Название асинхронный означает, что мы «теряем» контроль над порядком операций ввода-вывода. Порядок определяет операционная система, которая выстраивает операции, исходя из готовности устройств ввода-вывода.[1]

Асинхронный подход к написанию программы сложнее, но позволяет добиться большей эффективности. Примером может быть сравнение системного вызова epoll в Linux и Overlapped I/O в Microsoft Windows. epoll представляет собой пример неблокирующего синхронного ввода-вывода и опрашивает перечень файловых дескрипторов на готовность проведения операций. Он эффективен для сетевых операций ввода-вывода или различных видов межпроцессного взаимодействия, так как за этими операциями стоит копирование данных из и в буфера ядра, и не отнимает значительного времени у процессора. Однако этот системный вызов неэффективен с более медленным файловым вводом-выводом. Например: если в файле есть какие-то данные, то их чтение будет блокировать процесс до тех пор, пока они не будут прочитаны с диска и скопированы в предоставленный буфер. Подход Windows отличается: вы вызываете функцию ReadFile, передавая ей буфер для записи и файловый дескриптор. Данная функция лишь инициирует операцию чтения и мгновенно возвращает управление процессу. К тому времени как операционная система в фоне считает данные из файла в буфер, она просигнализирует процессу о завершении операции, либо через переданную в ReadFile функцию обратного вызова, либо через порт завершения ввода-вывода (IOCP). Функция обратного вызова будет вызвана лишь при ожидании завершения операции(й).[2]

Подходы к асинхронному вводу-выводу править

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

Функции обратного вызова править

Доступны в FreeBSD, OS X, VMS и Windows.

Потенциальная проблема в том, что глубина стека может расти неконтролируемо, поэтому необходимо только тогда запланировать другой ввод/вывод, когда завершён предыдущий. Если это должно быть удовлетворено немедленно, первоначальный callback не выполняет «раскрутку» стека до того, как следующий вызывается. Системы для предотвращения этого (такие как «средний план» ('mid-ground') планирование следующей работы) повышают сложность и снизижают производительность. На практике, однако, это, как правило, не является проблемой, так как следующий ввод/вывод будет сам по себе, как правило, возвращается, как только следующий ввода/вывода запущен, позволяя стеку, быть «раскрученным». Проблема также не может быть предотвращена путём избежания каких-либо дальнейших обратных вызовов, с помощью очереди, до первого возвращения обратного вызова.

Корутины править

Корутины (сопрограммы) позволяют писать асинхронные программы в синхронном стиле. Примеры:

  • async / await. Доступен в C#, Python, EcmaScript
  • Генераторы. Доступны в PHP, Python, Ruby с помощью ключевого слова yield

Также есть множество библиотек для создания сопрограмм (libcoro[3], Boost Coroutine)

Порты (очереди) завершения править

Доступен в Microsoft Windows, Solaris и DNIX. Запросы ввода/вывода выдаются асинхронно, но уведомления о выполнении предоставляются через механизм синхронизационной очереди в порядке их завершения. Обычно связано с конечным автоматом, структурирующим основной процесс (событийно-ориентированного программирование), который может иметь мало сходства с процессом, который не использует асинхронный ввод/вывод или что использование одной из прочих форм, что затрудняет повторное использование кода. Не требует дополнительных специальных механизмов синхронизации или потокобезопасных библиотек, равно как и текстовый (код) и временной (событие) потоки разделены.

Каналы ввода/вывода править

Доступные в мейнфреймах IBM, Groupe Bull и Unisys каналы ввода-вывода предназначены для максимального использования процессора и пропускной способности за счет выполнения ввода/вывода на сопроцессоре. Сопроцессор имеет на борту DMA, обслуживает прерывания устройства, контролируется центральным процессором, и прерывает основной процессор, только тогда, когда это действительно необходимо. Эта архитектура также поддерживает так называемые программы канала, которые работают на процессоре канала, чтобы делать тяжелую работу по деятельности ввода/вывода и протоколов.

Реализация править

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

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

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

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

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

Примеры править

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

1. Блокирующий, синхронный:

device = IO.open()
data = device.read() # процесс будет заблокирован, пока в device не появятся какие-либо данные
print(data)

2. Неблокирующий, синхронный:

device = IO.open()
while True:
    is_ready = IO.poll(device, IO.INPUT, 5) # ждём не более 5 секунд на наличие возможности чтения (INPUT) из устройства
    if is_ready:
        data = device.read() # процесс не будет заблокирован, так как мы убедились в возможности чтения
        break # выйти из цикла
    else:
        print("в устройстве нет данных!")

3. Неблокирующий, асинхронный:

ios = IO.IOService()
device = IO.open(ios)

def inputHandler(data, err):
    "Обработчик события наличия данных"
    if not err:
        print(data)

device.readSome(inputHandler)
ios.loop() # дожидаемся окончания операции, чтобы вызывать нужные обработчики. Если операций больше нет, то loop вернёт управление.

Также к асинхронному можно отнести паттерн реактор:

device = IO.open()
reactor = IO.Reactor()

def inputHandler(data):
    "Обработчик события наличия данных"
    print(data)
    reactor.stop()

reactor.addHandler(inputHandler, device, IO.INPUT)
reactor.run() # запускает реактор, который будет реагировать на события ввода-вывода и вызывать нужные обработчики

Примечания править

  1. Microsoft. Synchronous and Asynchronous I/O (англ.). Дата обращения: 21 сентября 2017. Архивировано 22 сентября 2017 года.
  2. msdn FileIOCompletionRoutine. Дата обращения: 21 сентября 2017. Архивировано 22 сентября 2017 года.
  3. libcoro. Дата обращения: 21 сентября 2017. Архивировано 2 декабря 2019 года.