Рабочим названием платформы .NET было |
Общие подходы к реализации приложений с параллельным выполнением операций
Асинхронный ввод-вывод
Обычные операции ввода-вывода происходят в синхронном режиме. Например, в приведенном ниже примере все операции выполняются строго последовательно: файл открывается, вызывается операция чтения данных, и только после того как все данные прочитаны, продолжается выполнение задачи:
#include <stdio.h> int sequential_io( char *filename, char *buffer, int size ) { FILE *fp; int done; fp = fopen( filename, "rb" ); if ( fp ) { done = fread( fp, 1, size, buffer ); /* код не будет выполняться, пока чтение не завершится*/ fclose( fp ); } else { done = 0; } buffer[done] = '\0'; return done; }
Такой подход прост в реализации, как с точки зрения операционной системы, так и с точки зрения пользователя ("пользователем" операционной системы в данном случае выступает разработчик). Однако во время выполнения операций ввода-вывода сама программа не выполняется - она ожидает завершения ввода-вывода. Во многих случаях такие операции, во-первых, занимают достаточно много времени и, во-вторых, выполняются специализированным оборудованием без участия центрального процессора, который в это время находится в состоянии ожидания и не выполняет никакой полезной работы (по крайней мере, с точки зрения данной программы, так как в общем случае начало ожидания приводит к перепланировке потоков).
Эффективность использования процессора можно было бы повысить, если бы существовала возможность выполнять код программы во время выполнения операций ввода-вывода. Конечно, это не всегда возможно или целесообразно. Например, если для продолжения работы программы необходимы данные, которые еще не получены, то нам все равно надо ожидать завершения ввода-вывода. Более того, структура приложения должна быть разработана с учетом специфики использования асинхронных операций ввода-вывода.
В Windows для реализации асинхронного ввода-вывода предусмотрены функции типа ReadFile, WriteFile, ReadFileEx, WriteFileEx и др. и специальная структура OVERLAPPED, которая используется для взаимодействия с асинхронной операцией. Асинхронные операции применяются следующим образом: перед началом операции заполняется структура OVERLAPPED и вызывается нужная функция для выполнения ввода-вывода, которая ставит операцию в очередь и немедленно возвращает управление вызвавшей программе. После этого программа продолжает свою работу независимо от хода выполнения операции ввода-вывода. При необходимости можно выяснить состояние асинхронной операции, дождаться ее завершения или отменить ее, не дожидаясь завершения. Для этого предназначен специальный набор функций, например, CancelIo, GetOverlappedResult, HasOverlappedIoCompleted и некоторые другие. Существует несколько вариантов использования асинхронного ввода-вывода; рассмотрим их на небольшом примере.
Для начала надо описать необходимые переменные и открыть файл с разрешением асинхронных операций ( FILE_FLAG_OVERLAPPED ):
OVERLAPPED ov; DWORD dwWritten; BYTE buffer[ 5000000 ]; HANDLE fh = CreateFile( "file.dat", FILE_READ_DATA|FILE_WRITE_DATA, FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES)NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, NULL ); if ( fh == INVALID_HANDLE_VALUE ) { /* возникла ошибка */ } ZeroMemory( &ov, sizeof(OVERLAPPED) ); FillMemory( buffer, sizeof(buffer), 123 );
- Выполнение асинхронных операций с опросом состояния; этот способ может обеспечить самую быструю реакцию на завершение операции ввода-вывода, но ценой более высокой загрузки процессора:
ov.Offset = 12345; if ( WriteFile( fh, buffer, sizeof(buffer), &dwWritten, &ov ) || GetLastError() == ERROR_IO_PENDING ) { /* пока операция ввода-вывода выполняется, выполняем некоторые операции. дожидаемся завершения операции ввода-вывода */ while (!GetOverlappedResult(fh, &ov, &dwWritten, FALSE)){} } else { /* возникла ошибка */ }
Функция GetOverlappedResult в данном случае проверяет состояние операции ввода-вывода и возвращает признак ее завершения. Опрос состояния операции в цикле позволяет наиболее быстро отреагировать на завершение операции (особенно на многопроцессорных машинах), однако ценой увеличения загрузки процессора, что может снизить общую производительность системы.
Функция WriteFile возвращает значение TRUE, если операция записи завершена синхронно, а в случае ошибки или начатой асинхронной операции она возвращает FALSE, поэтому требуется анализ кода возникшей "ошибки", которая может и не являться ошибкой.
- Выполнение асинхронных операций с ожиданием на объектах ядра:
ov.Offset = 12345; ov.hEvent = CreateEvent((LPSECURITY_ATTRIBUTES)NULL, TRUE, FALSE, 0); if ( WriteFile( fh, buffer, sizeof(buffer), &dwWritten, &ov ) || GetLastError() == ERROR_IO_PENDING ) { /* пока операция ввода-вывода выполняется, выполняем некоторые операции... дожидаемся завершения операции ввода-вывода */ GetOverlappedResult( fh, &ov, &dwWritten, TRUE ); } else { /* возникла ошибка */ }
Функция GetOverlappedResult в данном случае проверяет состояние операции и, если она еще не завершена, вызывает функцию WaitForSingleObject для ожидания завершения операции ввода-вывода. Объект "событие" можно не создавать - в этом случае функция будет ожидать освобождения объекта "файл"; однако в случае нескольких, накладывающихся друг на друга, асинхронных операций будет непонятно, какая именно операция завершилась, и использование специфичных для каждой операции событий снимает эту проблему.
Ожидание на объектах ядра является наиболее экономным, но реакция приложения на завершение ввода-вывода связана с работой планировщика, поэтому для достижения малых задержек иногда надо дополнительно повышать приоритеты потоков, переходящих в режим ожидания завершения ввода-вывода.
- Выполнение асинхронных операций с использованием функций завершения операций ввода-вывода. Эта функция будет автоматически вызвана после завершения ввода-вывода и может выполнить некоторые специальные действия. Процедура завершения обязательно вызывается в контексте того потока, который вызвал операцию ввода-вывода, - а для этого необходимо, чтобы поток был приостановлен, так как операционная система не должна прерывать работу активного потока. Следует перевести поток в состояние ожидания оповещения (alertable waiting) - при этом он не выполняется и может быть прерван для обработки процедуры завершения:
ov.Offset = 12345; if ( WriteFileEx( fh, buffer, sizeof(buffer), &ov, io_done ) ) { /* пока операция ввода-вывода выполняется, выполняем некоторые операции и переходим в режим ожидания оповещения, например, так: */ if ( SleepEx( INFINITE, TRUE ) != WAIT_IO_COMPLETION ) { /* ввод-вывод пока не завершен, возможно, ошибка */ } } else { /* возникла ошибка */ } /* нам еще надо предоставить собственную процедуру завершения ввода-вывода. В простейшем варианте она может ничего не делать: */ VOID CALLBACK io_done( DWORD dwErr, DWORD dwWritten, LPOVERLAPPED lpOv ) { ... }
Этот подход наиболее трудоемок и наименее распространен; на практике самым эффективным является механизм выполнения асинхронных операций с ожиданием на объектах ядра. Однако механизм вызова функций завершения (расширенный возможностью автоматического выбора потока, осуществляющего обработку функции завершения) послужил основой для реализации одного из очень эффективных механизмов взаимодействия потоков - порта завершения ввода-вывода.
При использовании асинхронного ввода-вывода необходимо очень внимательно следить за выделением и освобождением ресурсов - особенно памяти, занятой структурами OVERLAPPED, и буферами, участвующими в операциях ввода-вывода.
Асинхронный ввод-вывод является примером мультипроцессирования с использованием функционально различных устройств.