Важная правда об асинхронности в своей первозданной форме: нет никакого потока.
Тех, кто возразит, несть числа. «Нет», кричат они, «если я ожидаю операцию, должен быть поток, в котором выполняется ожидание! Возможно это поток из пула. Или поток операционной системы! Или что-то, связанное с драйвером устройства...»
Не внемлем этим крикам. Если операция по-настоящему асинхронная, то никакого потока нет.
Скептики не убеждены. Высмеем же их.
Проследим выполнение асинхронной операции вплоть до железа, уделив особое внимание платформе .NET и драйверу устройства. Нам придется упростить это описание, опустив некоторые детали, но мы не уйдем далеко от истины.
Рассмотрим некоторую операцию «записи» (в файл, сетевой поток, USB-тостер, куда угодно). Наш код прост:
Мы уже знаем, что UI поток не блокируется во время ожидания. Вопрос: существует ли другой поток, который приносит себя в жертву на Алтаре Блокировки, чтобы UI поток мог жить?
Держитесь. Нам придется глубоко нырнуть.
Первая остановка: библиотека (например, код BCL). Мы предполагаем, что WriteAsync реализован с использованием стандартной системы асинхронного ввода-вывода в .NET фреймворке, которая основана на overlapped I/O*. Т.о. запускается Win32 overlapped I/O операция с указанием ДЕСКРИПТОРА устройства.
ОС в свою очередь обращается к драйверу устройства и просит его начать операцию записи. Эта просьба представляет собой объект, который описывает операцию записи; такой объект называется пакетом запроса ввода-вывода (I/O Request Packet, IRP).
Драйвер получает IRP пакет и отдает устройству команду начать запись данных. Если устройство поддерживает режим прямого доступа к памяти (DMA), то выполнение команда заключается всего лишь в записи адреса буфера в регистр устройства. Это все, что может сделать драйвер; он помечает IRP пакет как «выполняющийся» и возвращает управление ОС.
Здесь заключается суть: драйверу устройства не разрешено блокировать управление во время обработки IRP пакета. Это значит, что если IRP пакет не может быть обработан немедленно, он должен быть обработан асинхронно. Это справедливо даже для синхронных методов! На уровне драйвера устройства все (нетривиальные) запросы являются асинхронными.
С IRP пакетом в статусе «выполняется» ОС возвращается в библиотеку, которая возвращает незавершенную задачу в обработчик нажатия кнопки, что в свою очередь приостанавливает выполнение метода, и UI поток продолжает свое исполнение.
Мы проследовали за запросом в самую бездну системы, до вплоть до физического устройства.
Теперь операция записи в процессе выполнения. Сколько потоков выполняют ее?
Нисколько.
Ни поток драйвера устройства, ни поток ОС, ни поток BCL, ни поток из пула не выполняют эту операцию записи. Нет никакого потока.
Теперь давайте проследим за ответом, следующим из владений демонов назад в мир смертных.
Через некоторое время после начала операции устройство завершает запись. И уведомляет об этом процессор при помощи прерывания.
Вызывается обработчик прерывания драйвера. Прерывание является событием уровня процессора, поэтому любая работа, которой был занят процессор, временно приостанавливается, вне зависимости от того, какой поток в данный момент выполнялся. Можно считать, что обработчик прерывания «одалживает» выполняющийся поток, однако я придерживаюсь мнения, что обработчики прерываний выполняются на таком низком уровне, что понятие «потока» не существует; они выполняются «под» всеми потоками, так сказать.
Если обработчик прерываний написан правильно, то все, что он делает, это говорит устройству «Спасибо за прерывание» и ставит в очередь объект отложенного вызова процедуры (Deferred Procedure Call, DPC).
Когда процессор заканчивает обрабатывать прерывания, он приступает к отложенным вызовам процедур. Они также исполняются на таком низком уровне, что говорит о «потоках» не совсем корректно; как и обработчики прерываний, отложенные вызовы процедур выполняются прямиком на центральном процессоре, «под» системой управления потоками.
DPC берет IRP пакет, который представляет собой запрос на запись, и помечает его как «завершенный». Однако этот статус существует только на уровне операционной системы; процесс имеет собственное адресное пространство, и его тоже нужно уведомить. Поэтому ОС создает специальный-уровня-ядра объект асинхронного вызова процедуры (APC) и помещает его в очередь того потока, который владеет ДЕСКРИПТОРОМ.
Поскольку библиотека/BCL использует стандартный механизм overlapped I/O, то она уже привязала дескриптор к порту завершения ввода-вывода (I/O Completion Port), который является частью пула потоков. Поэтому для выполнения APC используется поток из пула потоков ввода-вывода**, который и уведомляет задачу о том, что она завершена.
Поскольку задача захватила UI контекст, то асинхронный метод возобновляет свое выполнение не в потоке пула. Вместо этого, продолжение метода ставится в очередь на исполнение в UI контексте, и UI контекст возобновит выполнение метода, когда доберется до него.
Итак, мы видим, что пока операция выполнялась, не было никакого потока. Когда операция завершилась, разные потоки использовались для быстрого выполнения различных задач. Порядок времени выполнения таких задачи — от миллисекунды (например, выполнение APC в потоке пула) до микросекунды (например, обработка прерывания). Но никакой поток не был заблокирован, ожидая завершения операции.
Цепочка выполнения, которую мы проследили, является «стандартной», в чем-то упрощенной. Существует бесчисленное количество вариаций, однако суть остается той же.
Идея о том, что «где-то должен быть поток, выполняющий асинхронную операция», является ошибочной.
Освободите свой разум. Не пытайтесь найти этот «асинхронный поток» — это невозможно. Вместо этого, осознайте истину:
Нет никакого потока.
* — однозначного перевода на русский язык термина «overlapped I/O» мне не встречалось. Близким по смыслу является термин «асинхронный ввод-вывод» (прим. пер.).
** — в CLR существует два пула потоков: пул рабочих потоков и пул потоков ввода-вывода (прим. пер.).
Тех, кто возразит, несть числа. «Нет», кричат они, «если я ожидаю операцию, должен быть поток, в котором выполняется ожидание! Возможно это поток из пула. Или поток операционной системы! Или что-то, связанное с драйвером устройства...»
Не внемлем этим крикам. Если операция по-настоящему асинхронная, то никакого потока нет.
Скептики не убеждены. Высмеем же их.
Проследим выполнение асинхронной операции вплоть до железа, уделив особое внимание платформе .NET и драйверу устройства. Нам придется упростить это описание, опустив некоторые детали, но мы не уйдем далеко от истины.
Рассмотрим некоторую операцию «записи» (в файл, сетевой поток, USB-тостер, куда угодно). Наш код прост:
private async void Button_Click(object sender, RoutedEventArgs e)
{
byte[] data = ...
await myDevice.WriteAsync(data, 0, data.Length);
}
Мы уже знаем, что UI поток не блокируется во время ожидания. Вопрос: существует ли другой поток, который приносит себя в жертву на Алтаре Блокировки, чтобы UI поток мог жить?
Держитесь. Нам придется глубоко нырнуть.
Первая остановка: библиотека (например, код BCL). Мы предполагаем, что WriteAsync реализован с использованием стандартной системы асинхронного ввода-вывода в .NET фреймворке, которая основана на overlapped I/O*. Т.о. запускается Win32 overlapped I/O операция с указанием ДЕСКРИПТОРА устройства.
ОС в свою очередь обращается к драйверу устройства и просит его начать операцию записи. Эта просьба представляет собой объект, который описывает операцию записи; такой объект называется пакетом запроса ввода-вывода (I/O Request Packet, IRP).
Драйвер получает IRP пакет и отдает устройству команду начать запись данных. Если устройство поддерживает режим прямого доступа к памяти (DMA), то выполнение команда заключается всего лишь в записи адреса буфера в регистр устройства. Это все, что может сделать драйвер; он помечает IRP пакет как «выполняющийся» и возвращает управление ОС.
Здесь заключается суть: драйверу устройства не разрешено блокировать управление во время обработки IRP пакета. Это значит, что если IRP пакет не может быть обработан немедленно, он должен быть обработан асинхронно. Это справедливо даже для синхронных методов! На уровне драйвера устройства все (нетривиальные) запросы являются асинхронными.
Цитируя Тома Мудрости, «Вне зависимости от типа I/O запроса, все операции ввода-вывода, порученные драйверу приложением, выполняются асинхронно.»
С IRP пакетом в статусе «выполняется» ОС возвращается в библиотеку, которая возвращает незавершенную задачу в обработчик нажатия кнопки, что в свою очередь приостанавливает выполнение метода, и UI поток продолжает свое исполнение.
Мы проследовали за запросом в самую бездну системы, до вплоть до физического устройства.
Теперь операция записи в процессе выполнения. Сколько потоков выполняют ее?
Нисколько.
Ни поток драйвера устройства, ни поток ОС, ни поток BCL, ни поток из пула не выполняют эту операцию записи. Нет никакого потока.
Теперь давайте проследим за ответом, следующим из владений демонов назад в мир смертных.
Через некоторое время после начала операции устройство завершает запись. И уведомляет об этом процессор при помощи прерывания.
Вызывается обработчик прерывания драйвера. Прерывание является событием уровня процессора, поэтому любая работа, которой был занят процессор, временно приостанавливается, вне зависимости от того, какой поток в данный момент выполнялся. Можно считать, что обработчик прерывания «одалживает» выполняющийся поток, однако я придерживаюсь мнения, что обработчики прерываний выполняются на таком низком уровне, что понятие «потока» не существует; они выполняются «под» всеми потоками, так сказать.
Если обработчик прерываний написан правильно, то все, что он делает, это говорит устройству «Спасибо за прерывание» и ставит в очередь объект отложенного вызова процедуры (Deferred Procedure Call, DPC).
Когда процессор заканчивает обрабатывать прерывания, он приступает к отложенным вызовам процедур. Они также исполняются на таком низком уровне, что говорит о «потоках» не совсем корректно; как и обработчики прерываний, отложенные вызовы процедур выполняются прямиком на центральном процессоре, «под» системой управления потоками.
DPC берет IRP пакет, который представляет собой запрос на запись, и помечает его как «завершенный». Однако этот статус существует только на уровне операционной системы; процесс имеет собственное адресное пространство, и его тоже нужно уведомить. Поэтому ОС создает специальный-уровня-ядра объект асинхронного вызова процедуры (APC) и помещает его в очередь того потока, который владеет ДЕСКРИПТОРОМ.
Поскольку библиотека/BCL использует стандартный механизм overlapped I/O, то она уже привязала дескриптор к порту завершения ввода-вывода (I/O Completion Port), который является частью пула потоков. Поэтому для выполнения APC используется поток из пула потоков ввода-вывода**, который и уведомляет задачу о том, что она завершена.
Поскольку задача захватила UI контекст, то асинхронный метод возобновляет свое выполнение не в потоке пула. Вместо этого, продолжение метода ставится в очередь на исполнение в UI контексте, и UI контекст возобновит выполнение метода, когда доберется до него.
Итак, мы видим, что пока операция выполнялась, не было никакого потока. Когда операция завершилась, разные потоки использовались для быстрого выполнения различных задач. Порядок времени выполнения таких задачи — от миллисекунды (например, выполнение APC в потоке пула) до микросекунды (например, обработка прерывания). Но никакой поток не был заблокирован, ожидая завершения операции.
Цепочка выполнения, которую мы проследили, является «стандартной», в чем-то упрощенной. Существует бесчисленное количество вариаций, однако суть остается той же.
Идея о том, что «где-то должен быть поток, выполняющий асинхронную операция», является ошибочной.
Освободите свой разум. Не пытайтесь найти этот «асинхронный поток» — это невозможно. Вместо этого, осознайте истину:
Нет никакого потока.
* — однозначного перевода на русский язык термина «overlapped I/O» мне не встречалось. Близким по смыслу является термин «асинхронный ввод-вывод» (прим. пер.).
** — в CLR существует два пула потоков: пул рабочих потоков и пул потоков ввода-вывода (прим. пер.).