789
Операции.ввода-вывода.в.Windows
Представим реализацию веб-приложения, в которой для каждого пришедшего
на ваш сервер клиентского запроса формируется запрос к базе данных. При посту-
плении клиентского запроса поток из пула потоков обращается к вашему коду. При
выдаче синхронного запроса к базе данных этот поток окажется заблокированным на
неопределенное время, необходимое для получения ответа из базы. Если в это время
придет
еще один клиентский запрос, пул создаст еще один поток, который снова ока-
жется заблокированным. В итоге можно оказаться с целым набором блокированных
потоков, ожидающих ответа из базы данных. Получается, что веб-сервер выделяет
массу ресурсов (потоков и памяти для них), которые почти не используются!
Проблема усугубляется тем, что при получении результатов запросов из базы дан-
ных блокировка с потоков будет снята одновременно и все они начнут исполняться.
В
ситуации, когда количество потоков значительно превосходит количество ядер
процессора, операционная система прибегнет к частым переключениям контекста,
что значительно снизит производительность. Так что это не тот путь, который по-
зволил бы реализовать масштабируемое приложение.
Теперь рассмотрим процедуру выполнения асинхронных операций ввода-вывода
в Windows (рис. 28.2). Я убрал со схемы все внешние устройства, кроме жесткого
диска, а также добавил пул потоков среды CLR и слегка отредактировал код. Откры-
тие файла по-прежнему выполняется путем создания объекта
FileStream
, но теперь
ему передается флаг
FileOptions.Asynchronous
, который указывает Windows, что
операции чтения из файла и записи в файл следует выполнять асинхронно.
Чтение данных из файла теперь выполняется методом
BeginRead
, а не методом
Read
.
ReadAsync
создает
объект
Task
, представляющий незавершенную
операцию чтения, а затем вызывает Win32-функцию
ReadFile
(1), которая выделяет
место под IRP-пакет, инициализирует его, как и в предыдущем сценарии (2), и пере-
дает в ядро Windows (3). Windows добавляет IRP-пакет в IRP-очередь драйвера
жесткого диска (4), но на этот раз поток не блокируется, а немедленно возвращает
управление после вызовов метода
BeginRead
(5, 6 и 7). Конечно, это может произой-
ти еще до обработки IRP-пакета, поэтому после
ReadAsync
не может следовать код,
который пытается обратиться к байтам в переданном методу массиве
Byte[]
.
Может возникнуть вопрос, когда и каким образом обрабатываются считываемые
данные? При вызове
ReadAsync
возвращается объект
Task
. Используя этот
объект, можно вызвать метод
ContinueWith
для регистрации метода обратного вызо-
ва, который должен выполняться при завершении задачи, а затем обработать данные
в методе обратного вызова. Также можно использовать асинхронные функции C#,
позволяющие использовать последовательную структуру кода (как при выполнении
синхронного ввода-вывода).
Закончив обработку IRP-пакета (
a
), устройство помещает делегат в очередь
CLR-пула потоков (
b
). В дальнейшем какой-то из потоков пула берет готовый IRP-
пакет и активизирует метод обратного вызова (
c
)
1
. В результате вы узнаете о за-
1
Готовые IRP-пакеты извлекаются из пула по алгоритму «первым пришел — первым
обслужен».