» Асинхронный ввод/вывод Win Api. . . Блог программистов


Блог программистов






200715 Май

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

    Сегодня я расскажу про асинхронный ввод/вывод. При обычном вводе/выводе функция ввода/вывода (FileWrite/Read, Write/ReadFile(Ex), BlockRead/Write, TFileStream.Read/Write) возвратит управление только после того, как полностью выполнится операция ввода/вывода. А если используется носитель с медленной скоростью чтения/записи, или производится обработка больших объёмов данных, то программа «подвиснет» на время чтения/записи. При асинхронном вводе/выводе функция сразу же возвращает управление, и программа продолжает выполняться дальше без задержек. Эта технология может пригодиться для разработки программ для взаимодействия с внешними устройствами с низкой скоростью передачи данных, например сотовыми телефонами, устройствами BlueTooth или IrDA.

    Итак, приступим. Асинхронный ввод/вывод не может осуществить ни одна из родных Delphi функций: ни функции FileRead/FileWrite, ни функции, доставшиеся нам от pascal BlockRead/BlockWrite, ни класс TFileStream. Асинхронный ввод/вывод можно осуществить только через API функции.
   Взаимодействие с файлами через API осуществляется с помощью функций: CreateFile, ReadFile/WriteFile, CloseHandle. Так как не все знают, как работать с файлами через API, то вскользь вспомним каждую из них.
    Функция CreateFile. С помощью этой функции можно
открыть на чтение или запись многие объекты: порты COM, дисковые устройства (для
прямой работы с диском), пайпы и т.д.

HANDLE CreateFile(
 LPCTSTR lpFileName, // имя файла
 DWORD dwDesiredAccess, // тип доступа 
 DWORD dwShareMode, // параметры общего доступа
 LPSECURITY_ATTRIBUTES lpSecurityAttributes,//атрибуты защиты
 DWORD dwCreationDistribution, // создавать или открывать
 DWORD dwFlagsAndAttributes, // дополнительные атрибуты 
 HANDLE hTemplateFile // нужен при создании файлов 
);

    lpFileName путь к файлу, точнее указатель на строку
заканчивающуюся знаком #0.
   dwDesiredAccess тип запрашиваемого доступа может быть:
1. 0 нет доступа, можно получить только атрибуты файла
2. GENERIC_READ только чтение
3. GENERIC_WRITE только запись
4. GENERIC_READ or GENERIC_WRITE полный доступ
    dwShareMode параметр общего доступа если указан 0 то больше никакой другой процесс не сможет получить доступ к файлу до тех пор пока хендл файла не будет закрыт. Может также принимать значения или их комбинации:
1. FILE_SHARE_READ другие процессы могут получать доступ к файлу, только если запрошен тип доступа GENERIC_READ
2. FILE_SHARE_WRITE другие процессы могут получать доступ к файлу только если запрошен тип доступа GENERIC_WRITE.
    lpSecurityAttributes указатель структуру TSecurityAttributes с атрибутами защиты, в большинстве случаев можно указывать 0.
    dwCreationDistribution тип открытия файла может принимать значения:
1. CREATE_NEW файл будет создан, если он уже существует, то функция возвратит ошибку
2. CREATE_ALWAYS файл будет создан в любом случае, т.е. если файл существует, то он будет удалён и снова создан, ошибка может произойти, только если файл занят
3. OPEN_EXISTING файл будет открыт, если его нет либо файл занят, то функция возвратит ошибку.
4. OPEN_ALWAYS файл будет, открыть, если его нет, то он будет создан.
    dwFlagsAndAttributes дополнительные флаги, если файл cоздаётся то можно указать следующие флаги
FILE_ATTRIBUTE_ARCHIVE
FILE_ATTRIBUTE_HIDDEN
FILE_ATTRIBUTE_NORMAL
FILE_ATTRIBUTE_READONLY
FILE_ATTRIBUTE_SYSTEM
И многие другие, нас интересует открытие файлов и флаг, указываемый только при открытии FILE_FLAG_OVERLAPPED. Если этот флаг указан, то с файлом (или устройством) можно работать в асинхронном режиме.
    hTemplateFile при создании файлом здесь можно указать хендл файла, атрибуты которого будут взять за основу создаваемого файла, у созданного файла атрибуты будут такие же. У нас этот параметр будет равен 0.
    При успешном вызове функция возвратит хендл файла. При ошибке функция возвращает значение -1 или INVALID_HANDLE_VALUE. Для получения кода ошибки надо вызвать GetLastError.
    Пример открытия файла с поддержкой асинхронного вывода/вывода:

FileHandle := CreateFile('file.dat', GENERIC_WRITE, 0,0, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,0);
if FileHandle=INVALID_HANDLE_VALUE then
 begin
  ShowMessage('ошибка');
  exit;
 end;

    Закрытие хендла осуществляет функция CloseHandle. Теперь функции чтения/записи.
    Функция ReadFile/WriteFile осуществляет чтение/запись (далее чтение) из файла (устройства) начиная с текущей позиции после окончания чтения обновляет указатель в файле.

BOOL ReadFile(
 HANDLE hFile, // хендл файла 
 LPVOID lpBuffer, //указатель на буфер 
 DWORD nNumberOfBytesToRead, // размер данных 
 LPDWORD lpNumberOfBytesRead, //размер считанных данных
 LPOVERLAPPED lpOverlapped //структура OVERLAPPED
);

Функция полностью идентична функции BlockRead за исключением последнего параметра.
    lpOverlapped указатель структуру OVERLAPPED для возможности работы в асинхронном режиме. Если при открытии не указан флаг FILE_FLAG_OVERLAPPED, то функция работает точно так же как и BlockRead.
    Хотелось сделать примечание про второй и четвёртый параметр. В заголовочных файлах Delphi (windows.pas) у этой функции второй параметр задан как константа, это говорит компилятору, что надо указывать указатель на буфер при компиляции, так как реально функция принимает указатель. Программисту надо просто указывать переменную буфер вместо указателя. Четвёртый параметр указан как var, это тоже говорит компилятору указывать указатель переменную при компиляции.
Теперь ближе к делу. Структура OVERLAPPED

typedef struct _OVERLAPPED {
 DWORD Internal; 
 DWORD InternalHigh; 
 DWORD Offset; 
 DWORD OffsetHigh; 
 HANDLE hEvent; 
} OVERLAPPED;

    Поля Internal и InternalHigh используются системой. И трогать
их нежелательно. Однако по полю Internal можно узнать завершилась операция или нет, но об этом позже. При асинхронном вводе/выводе функции передачи данных игнорируют текущий указатель в файле, указатель в файле извлекается из полей Offset и OffsetHigh, поле OffsetHigh нужно для файлов размером больше чем 4 ГБ. Поле hEvent должно содержать хендл объекта события, с помощью которого можно узнать завершилась ли запись/чтение данных и для возможности.
    Теперь по порядку. При вызове функции чтения/записи (примеры буду приводить на операциях записи) функция сразу возвращает управление со значение FALSE и с кодом ошибки ERROR_IO_PENDING. Объект событие, указанное в поле hEvent переводится в так называемое «сигнальное» состояние только после завершения операции. Если надо подождать завершения операции надо использовать функцию WaitForSingleObject.
    Функция WaitForSingleObject возвращает управление в двух случаях: после перехода объекта в сигнальное состояние, после истечения интервала.

DWORD WaitForSingleObject(
 HANDLE hHandle, // хендл объекта  DWORD dwMilliseconds // интервал в миллисекундах );

    Как узнать, что произошло: переход в сигнальное состояние или истечение интервала? Если функция возвратила значение WAIT_OBJECT_0, то объект перешёл в сигнальное состояние, если значение WAIT_TIMEOUT значит, истёк интервал времени.
    При использовании асинхронного ввода/вывода функции игнорируют четвёртый параметр. А как нам узнать, сколько реально байт записано, и вообще успешность операции (а вдруг носитель повреждён, а вдруг места на носителе не хватает)? Для того чтобы узнать успешность операции используется функция GetOverlappedResult:

BOOL GetOverlappedResult(
 HANDLE hFile, //хендл файла (устройства)
 LPOVERLAPPED lpOverlapped, //структура OVERLAPPED
 LPDWORD lpNumberOfBytesTransferred,//число записанных байт
 BOOL bWait // флаг ожидания 
); 

    Параметр lpNumberOfBytesTransferred должен содержать указатель на переменную, в которой сохраниться число реально записанных байт, но благодаря Delphi надо просто указать переменную. Если в параметре bWait указано TRUE и операция ещё не завершена, то функция подождёт пока операция не завершится. Если указано FALSE и операция ещё не завершена, то функция не будет ждать завершения, код ошибки в данном случае будет ERROR_IO_INCOMPLETE. После вызова данной функции для того, что бы получить код ошибки надо вызвать GetLastError. Следует подметить, что вызывать функцию GetLastError имеет смысл, только если функция GetOverlappedResult вернула нулевое значение.
    Как создать объект-событие? Для этого используется функция CreateEvent:

HANDLE CreateEvent(
 LPSECURITY_ATTRIBUTES lpEventAttributes,//атрибуты безопасности
 BOOL bManualReset, // парметр ручного сброса события
 BOOL bInitialState, //начальное состояние 
 LPCTSTR lpName // имя объекта 
);

    Поле lpEventAttributes можно не указывать, так же как поле lpName т.е. указывать 0. Параметр bManualReset отвечает за ручной сброс события, если он равен TRUE, то вы должны сами сбрасывать объект в несигнальное состояние с помощью функции ResetEvent, если FALSE, то система автоматически будет сбрасывать с несигнальное состояние после каждого возвращения из ожидания. Параметры bInitialState задаёт начальное состояние объекта если FALSE, то начальное состояние несигнальное.
    В MS SDK есть описание функции HasOverlappedIoCompleted, с помощью которой можно узнать завершена ли операция.

BOOL HasOverlappedIoCompleted(
 LPOVERLAPPED lpOverlapped 
);

Но её нет в заголовочных файлах Delphi. Конечно же, я ошибся сказав что HasOverlappedIoCompleted это функция, на самом это макрос:

#define HasOverlappedIoCompleted(lpOverlapped) \ 
 ((lpOverlapped)->Internal != STATUS_PENDING)

Напишем такую функцию на Delphi

function HasOverlappedIoCompleted(OVR: TOverlapped):boolean;
begin
 Result:= OVR.InternalSTATUS_PENDING
end;

    Теперь ближе к практике. Для примера напишем код, который записывает на дискету файл размером 1.44 МБ (по моему это наиболее лучший пример для асинхронного вывода).
const

 FLOPP_S=1457664;
var
 arr:array[1..FLOPP_S] of byte;

procedure TForm1.Button2Click(Sender: TObject);
var
 FileHandle,DestFileHandle:THandle;
Overlap: TOverlapped; res:BOOL; _WRITED,i,LastError:DWORD; begin FillChar(Overlap,SizeOf(Overlap),0); {заполним массив некоторыми данными для проверки, например буквой Q через каждые 100 байт} for i:=1 to trunc(FLOPP_S/100) do arr[i*100]:=ord('Q'); FileHandle :=CreateFile('a:\file.dat', GENERIC_WRITE, 0,0, OPEN_EXISTING,FILE_FLAG_OVERLAPPED,0); if FileHandle=INVALID_HANDLE_VALUE then begin ShowMessage('нет'); exit; end; Overlap.hEvent:=CreateEvent(0,True,False,0); res:=WriteFile(FileHandle,arr,FLOPP_S,_WRITED,@Overlap); LastError := GetLastError; ShowMessage('Запись началась'); if not res then begin if LastError = ERROR_IO_PENDING then while not HasOverlappedIoCompleted(Overlap) do Application.ProcessMessages else ShowMessage('Неизвестная ошибка'); end; GetOverlappedResult(FileHandle,Overlap,_WRITED,FALSE); ShowMessage('Запись закончена! Записано байт '+IntToStr(_WRITED)); CloseHandle(Overlap.hEvent); CloseHandle(FileHandle); end;

В этом примере я использовал функцию HasOverlappedIoCompleted. Можно использовать функцию WaitForSingleObject, изменения будут небольшие:


if not res then
 if LastError = ERROR_IO_PENDING then
  while WaitForSingleObject(Overlap.hEvent,100)=WAIT_TIMEOUT do
   Application.ProcessMessages
                                              else
 ShowMessage(' Неизвестная ошибка ');

    Специально для выполнения асинхронных операций созданы функции ReadFileEx и WriteFileEx.
    Функция WriteFileEx (ReadFileEx идентична) выполняет асинхронную запись. Её можно применить только для файлов открытых с флагом FILE_FLAG_OVERLAPPED.

BOOL WriteFileEx(
 HANDLE hFile, // хендл
 LPCVOID lpBuffer, // указатель буффер
 DWORD nNumberOfBytesToWrite, // размер данных
 LPOVERLAPPED lpOverlapped, // всем известная структура
 LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 
);

Как видно параметра подобного lpNumberOfByteswrite нет, а он и не нужен. Хочу подметить, что этой функции в отличие от старой надо передавать УКАЗАТЕЛЬ на буфер. Теперь о последнем параметре, lpCompletionRoutine должен содержать указатель на функцию:

VOID WINAPI FileIOCompletionRoutine(
 DWORD dwErrorCode, // код ошибки 
 DWORD dwNumberOfBytesTransfered,// обработанное количество байт
 LPOVERLAPPED lpOverlapped //структура OVERLAPPED 
);

Эта функция вызовется, когда операция ввода/вывода завершится. Эта функция может быть вызвана только в режиме ожидания, т.е. только тогда когда было вызвано WaitForSingleObjectEx. У этой функции появился третий параметр, который должен быть равен TRUE, если же он имеет значение FALSE, то функция больше не вернёт управление потоку, вызвавшему её, а только функции указанной в lpCompletionRoutine. Не рекомендуется вызывать функцию WaitForSingleObject так как она является оболочкой вокруг WaitForSingleObjectEx и она вызывает её с третьи параметром равным FALSE. (Хочу подметить, что почти все обычные функции являются оболочками вокруг своих Ex функций). Если вы не используете функцию WaitForSingleObjectEx, то функция не будет вызвана вообще. Так же она может быть вызвана при использовании функции WaitForMultipleObjectsEx. Объявление функции lpCompletionRoutine на Delphi:

procedure EndWrite(dwErrorCode:DWORD; dwNumberOfBytesTransfered:DWORD; lpOverlapped:POverlapped);stdcall;

    Наконец пример использования функции WriteFileEx:

procedure EndWrite(dwErrorCode:DWORD; dwNumberOfBytesTransfered:DWORD; lpOverlapped:POverlapped);stdcall;
begin
 ShowMessage(' Всё готово! записано байт '+IntToStr(dwNumberOfBytesTransfered));
end;

procedure TForm1.Button4Click(Sender: TObject);
var
 FileHandle,DestFileHandle:THandle;
 Overlap: TOverlapped;
 res:BOOL;
 _WRITED,i,LastError:DWORD;
begin
 FillChar(Overlap,SizeOf(Overlap),0);
 for i:=1 to trunc(FLOPP_S/100) do
  arr[i*100]:=ord('Q');
 FileHandle:= CreateFile('a:\file.dat',GENERIC_WRITE, 0,0,OPEN_EXISTING, FILE_FLAG_OVERLAPPED,0);
 if FileHandle=INVALID_HANDLE_VALUE then
  begin
   ShowMessage(' не прёт ');
   exit;
  end;
 Overlap.hEvent :=CreateEvent(0,True, False,0);

 res:= WriteFileEx(FileHandle, @arr,FLOPP_S, Overlap,@EndWrite);
 LastError := GetLastError;
 ShowMessage(' Запись началась ');
 while WaitForSingleObjectEx(Overlap.hEvent, 1000,True)=WAIT_TIMEOUT do
  Application.ProcessMessages ;

 GetOverlappedResult(FileHandle,Overlap,_WRITED,FALSE); 
 ShowMessage(' Запись закончена! записано байт '+IntToStr(_WRITED));
 CloseHandle(Overlap.hEvent);
 CloseHandle(FileHandle);

end;

    Вот и всё на сегодня. В архиве исходник с 3-мя примерами работы с асинхронным вводом/выводом.
 
Скачать исходник

Комментарии

  1. Mixas
    30 мая, 2007 | 08:21

    Хорошая статья.

  2. decay
    6 мая, 2008 | 15:47

    Большое спасибо за статью, всё понятно и по делу.

  3. Sergey
    22 мая, 2008 | 14:55

    Хорошо написано, спасибо!

  4. 23 января, 2009 | 22:37

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

    Сейчас пишу программу на Visual C++ и ваша статья очень пригодилась, в отличии от мегабайтов мути на С сайтах.

  5. 21 марта, 2009 | 20:00

    Спасибо! Прекрасная статья! Правда не совсем понятно что тут: FillChar(Overlap,SizeOf(Overlap),0);

  6. --Shark--
    7 апреля, 2009 | 15:09

    FillChar(Overlap,SizeOf(Overlap),0);

    Заполняет нулями область памяти. Первый параметр адрес начала, второй количество байт, третий собствено чем заполняется…
    Вместо FillChar(Overlap,SizeOf(Overlap),0); можно использовать ZeroMemory(@Overlap, Sizeof(Overlap));

  7. Сергей Суевалов
    28 сентября, 2009 | 06:07

    Была большая проблема — связь с аппаратом начала 1990 годов по RS232 под ХР. Почти отчаялся, но, прочитав Вашу статью, и без особого труда — всё очень хорошо получилось и всё хорошо работает! Громадное спвсибо!

  8. Владимир
    19 ноября, 2009 | 22:48

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

  9. Сергей
    22 февраля, 2010 | 16:18

    Скачал исходник.. Функция возвращает управление, только после окончания записи на дискету 🙁
    Бардак

  10. rpy3uH
    23 февраля, 2010 | 17:52

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

  11. имя (обязательно)
    7 ноября, 2010 | 13:29

    бобра автору! всё расписано лучше некуда, батя грит малаца

  12. Серж
    14 ноября, 2011 | 12:43

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

  13. Макс
    14 января, 2012 | 15:14

    Так и не понял как сделать асинхронную запись файла
    У меня вот так WriteFile(myFile1, buf2, i,wr,nil);
    если заменить nil на FILE_FLAG_OVERLAPPED то компилятор выдает ошибку(мол там должон быть интежер)
    пробовал ставить WriteFileEx — тоже самое
    открывал CreateFile с этим флагом все равно ошибка компилятора (дельфи 7)

  14. Санек
    15 марта, 2012 | 10:41

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

  15. Санек
    15 марта, 2012 | 10:46

    прошу прощения, не событие прихода завершающего символа а событие завершения записи