●Win32API(C言語)編 第52章 非同期的なファイルの読み書き

'2006/12/27 VisualC++2005 ExpressEdition への対応。

○非同期I/O

前章でReadFile()やWriteFile()を使ったファイルの読み書きを説明しました。 前章で行った方法は、いわゆる「同期I/O」です。「I/O」というのは、 「Input/Output」、つまりは「入出力」という意味です。

例えば、WriteFile()で同期書き込みを行うと、要求した書き込み操作が完了するまで、WriteFile()から 処理が戻ってきません。つまり、巨大なデータの書き込み要求を行うと、非常に時間がかかるため、WriteFile() を呼び出したまま、長時間、プログラムは停止した状態になります。

それに対し、とりあえず書き込み操作の要求だけ出しておいて、さっさと処理を戻す方法もあります。 それが「非同期I/O」です。非同期I/Oの場合、巨大なデータであっても、 すぐに処理が戻ります。書き込み処理は、少しずつ少しずつ背後で行われます。その間、他の処理も同時に 行えるため、プログラムが停止してしまうことがありません。

非同期I/Oを行うためには、幾つか方法があります。ここでは、前章と同じReadFile()/WriteFile()を 使って、非同期I/Oを試してみます。前章で無視していた第5引数(オーバーラップ構造体のアドレス)を 使用します。

○オーバーラップ構造体

ReadFile()やWriteFile()の第5引数は、オーバーラップ構造体のアドレスです。使用しないのならNULL で構わないのですが、非同期I/Oを行う場合には指定することになります。オーバーラップ構造体の具体的な 型はOVERLAPPED型です。

typedef struct _OVERLAPPED{
	ULONG_PTR Internal;
	ULONG_PTR InternalHigh;
	DWORD     Offset;
	DWORD     OffsetHigh;
	HANDLE    hEvent;
}OVERLAPPED, *LPOVERLAPPED;

InternalとInternalHighは無視して構いません。「Internal」とは「内部の」というような意味合いで、 Windows側が内部的に使う領域です。OffsetとOffsetHighは、読み書きの開始位置を、ファイルの先頭からの バイト数で表したものです。Offsetは下位32バイト、OffsetHighは上位32バイトをセットします。 hEventは、イベントオブジェクトと呼ばれるオブジェクトのハンドルです。 これはイベントオブジェクトを使ったことが無いと分からないと思いますが、ここにハンドルを指定しておくと、 非同期I/Oが完了したときに知らせてくれます。とりあえずNULLでも構いません。

この構造体のうち、InternalとInternalHighを除く3つのメンバをセットした後、ReadFile()なりWriteFile() なりに渡せば、非同期I/Oの準備が整います。例えば、次のようにします。

OVERLAPPED ol;
ZeroMemory( &ol, sizeof(ol) );  // まとめてゼロクリア
ol.offset = 0;                  // 必要に応じてセット
ol.offsetHight = 0;             // 必要に応じてセット
ol.hEvent = NULL;               // 必要に応じてセット
ReadFile( hFile, buf, size, &readSize, &ol );

本当は、この前にCreateFile()でファイルを開き、ファイルハンドルを取得しておく必要があります。 その際、CreateFile()の第6引数にFILE_FLAG_OVERLAPPEDというフラグを 指定しておかなければなりません。このフラグを指定することにより、そのファイルをOVERLAPPED構造体を 使った非同期I/Oで読み書きすることを指示します。このフラグを指定したからには、必ず非同期I/Oを 行わなければならないことにも注意して下さい。


ところで、非同期I/Oの場合は色々と厄介な問題があります。問題が起こる大きな原因は、非同期I/Oでは、 いつ処理が終わるか分からないという点です。同期的な処理なら、処理が完了するまで関数から戻ってこない ので、逆に言えば、関数から戻ってきたときには確実に処理が完了していると言い切れます。しかし、非同期 的な処理ではそうはいきません。関数を呼び出してから、数秒後に処理が完了しているとは言い切れません。 いつ終わるかは、そのときのコンピュータの状況にもよりますから(他に色々と処理が行われていたら、その 分遅くなるでしょう)、プログラムのテスト中にはたまたま1秒で終わったとしても、次回起動時には3秒 かかるかも知れません。

また、非同期I/O中は、先程のOVERLAPPED構造体を使っています。ですから、非同期I/Oの処理が完了する までの間、この構造体はメモリ上に確実に存在していなければなりません。OVERLAPPED構造体を、通常のローカル 変数として作ってはならないということになります。malloc()やnew演算子を使うのが簡単な解決策ですが、 非同期I/O処理の完了後に、確実にfree()やdelete演算子を呼び出して解体する必要があります。

○処理の完了を知る方法

前述したように、非同期I/Oでは処理がいつ終わるか分からないので、完了したかどうかを知る方法が 必要になります。方法は幾つか存在します。まず、直接的に、処理が完了しているかどうかを聞く方法 ですが、その場合、HasOverlappedIoCompletedマクロを使います。

BOOL HasOverlappedIoCompleted(LPOVERLAPPED lpOverlapped);

関数的な記述をすると上のようになります。引数にOVERLAPPED構造体のアドレスを渡します。もし、処理が 完了していたらTRUEが、未完了ならFALSEが返されます。ちなみにこのマクロは、OVERLAPPED構造体のInternalメンバ を参照しています。Internalな部分は、このように情報の格納のために用いられ、その値をプログラマが直接 見ることはありません。このようなマクロを通じて参照するようになっています。

また、非同期I/Oの状態を調べるためにGetOverlappedResult()という関数 もあります。

BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpNumberOfBytesTransferred, BOOL bWait);

第1引数にファイルハンドル、第2引数にOVERLAPPED構造体のアドレス、第3引数に処理済みのバイト数 を受け取る変数のアドレス、第4引数にフラグを渡します。第4引数のフラグは、もし非同期I/Oがまだ完了 していなかった場合、TRUEをしてあると、完了するまで関数の内部で待ちます。FALSEの場合は、待たずに 関数から戻ってきます。

○ソース例

では実験してみます。

#include <windows.h>
#include <tchar.h>

#define ASYNC_MODE              // 非同期I/Oを試すとき定義する

// 定数
#define WINDOW_WIDTH  (400)		// ウィンドウの幅
#define WINDOW_HEIGHT (300)		// ウィンドウの高さ
#define WINDOW_X ((GetSystemMetrics( SM_CXSCREEN ) - WINDOW_WIDTH ) / 2)
#define WINDOW_Y ((GetSystemMetrics( SM_CYSCREEN ) - WINDOW_HEIGHT ) / 2)

// グローバル変数
HINSTANCE g_hInst;              // インスタンスハンドル
OVERLAPPED g_ol;                // オーバーラップ構造体
BYTE g_buf[40*1000*1000];       // 巨大なバッファ
HANDLE g_hFile;                 // ファイルハンドル

// プロトタイプ宣言
HWND Create(HINSTANCE hInst);
void StartRead(const TCHAR* filename);
void Message(const TCHAR* mes);
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);


// 開始位置
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR pCmdLine, int showCmd)
{
	HWND hWnd;
	MSG msg;
	DWORD bytes = 0;

	g_hInst = hInst;  // インスタンスハンドルをグローバル変数に保存しておく

	// ウィンドウを作成する
	hWnd = Create( hInst );
	if( hWnd == NULL )
	{
		MessageBox( NULL, _T("ウィンドウの作成に失敗しました"), _T("エラー"), MB_OK );
		return 1;
	}

	// ウィンドウを表示する
	ShowWindow( hWnd, SW_SHOW );
	UpdateWindow( hWnd );

	// メッセージループ
	while( 1 )
	{
#ifdef ASYNC_MODE
		// 非同期I/Oの状態を確認する
		if( g_hFile != NULL )
		{
			if( GetOverlappedResult( g_hFile, &g_ol, &bytes, FALSE ) )
			{
				Message( _T("完了") );
				CloseHandle( g_hFile );
				break;
			}
			else
			{
				if( GetLastError() != ERROR_IO_INCOMPLETE )
				{
					Message( _T("エラー") );
					break;
				}
			}
		}
#endif

		if( GetMessage( &msg, NULL, 0, 0 ) == 0 )  // メッセージを取得する
		{
			// アプリケーションを終了させるメッセージが来ていたらループを抜ける
			break;
		}
		else
		{
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}
	}

	return 0;
}

// ウィンドウを作成する
HWND Create(HINSTANCE hInst)
{
	WNDCLASSEX wc;

	// ウィンドウクラスの情報を設定
	wc.cbSize = sizeof(wc);               // 構造体サイズ
	wc.style = CS_HREDRAW | CS_VREDRAW;   // スタイル
	wc.lpfnWndProc = WndProc;             // ウィンドウプロシージャ
	wc.cbClsExtra = 0;                    // 拡張情報1
	wc.cbWndExtra = 0;                    // 拡張情報2
	wc.hInstance = hInst;                 // インスタンスハンドル
	wc.hIcon = (HICON)LoadImage(          // アイコン
		NULL, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON,
		0, 0, LR_DEFAULTSIZE | LR_SHARED
	);
	wc.hIconSm = wc.hIcon;                // 子アイコン
	wc.hCursor = (HCURSOR)LoadImage(      // マウスカーソル
		NULL, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR,
		0, 0, LR_DEFAULTSIZE | LR_SHARED
	);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); // ウィンドウ背景
	wc.lpszMenuName = NULL;                     // メニュー名
	wc.lpszClassName = _T("Default Class Name");// ウィンドウクラス名
	
	// ウィンドウクラスを登録する
	if( RegisterClassEx( &wc ) == 0 ){ return NULL; }

	// ウィンドウを作成する
	return CreateWindow(
		wc.lpszClassName,      // ウィンドウクラス名
		_T("Sample Program"),  // タイトルバーに表示する文字列
		WS_OVERLAPPEDWINDOW,   // ウィンドウの種類
		WINDOW_X,              // ウィンドウを表示する位置(X座標)
		WINDOW_Y,              // ウィンドウを表示する位置(Y座標)
		WINDOW_WIDTH,          // ウィンドウの幅
		WINDOW_HEIGHT,         // ウィンドウの高さ
		NULL,                  // 親ウィンドウのウィンドウハンドル
		NULL,                  // メニューハンドル
		hInst,                 // インスタンスハンドル
		NULL                   // その他の作成データ
	);
}

// ファイル読み込み開始
void StartRead(const TCHAR* filename)
{
	static DWORD readSize;

	// 巨大なファイルを開く
#ifdef ASYNC_MODE
	g_hFile = CreateFile( filename, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL );
#else
	g_hFile = CreateFile( filename, GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL );
#endif

	if( g_hFile != INVALID_HANDLE_VALUE )
	{
#ifdef ASYNC_MODE
		// オーバーラップ構造体の初期化
		ZeroMemory( &g_ol, sizeof(g_ol) );
		g_ol.Offset = 0;
		g_ol.OffsetHigh = 0;
		g_ol.hEvent = NULL;

		// 読み込み開始
		if( ReadFile( g_hFile, g_buf, sizeof(g_buf), &readSize, &g_ol ) )
#else
		// 読み込み開始
		if( ReadFile( g_hFile, g_buf, sizeof(g_buf), &readSize, NULL ) )
#endif
		{
			// 非同期I/Oの場合も、ReadFile()がすぐにTRUEを返す可能性がある
			Message( _T("完了") );
			CloseHandle( g_hFile );
			g_hFile = NULL;
		}
		else
		{
			// ReadFile()がFALSEを返したとしても、非同期I/Oの場合はエラーではない可能性がある
			if( GetLastError() != ERROR_IO_PENDING )
			{
				// GetLastError()で調べて、ERROR_IO_PENDINGなら非同期I/O中であるが、それ以外
				// の場合は、本当にエラーである
				Message( _T("エラー") );
			}
		}
	}
}

// メッセージボックスを出す
void Message(const TCHAR* mes)
{
	MessageBox( NULL, mes, _T("メッセージ"), MB_OK );
}

// ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
	switch( msg )
	{
	case WM_CREATE:			// ウィンドウが作成されたとき
		// 読み込み開始
		StartRead( _T("test.dat") );
		return 0;

	case WM_DESTROY:        // ウィンドウが破棄されるとき
		PostQuitMessage( 0 );
		return 0;
	}

	return DefWindowProc( hWnd, msg, wp, lp );
}

ちょっと複雑な作りをしていますが、WM_CREATEメッセージの処理ルーチンで、StartRead()を呼び出し、その 内部でファイルの読み込みを開始しています。ソースの先頭にあるASYNC_MODEが定義されていれば、非同期I/Oを 行い、定義されていなければ通常の同期I/Oを行います。両者の違いをよく確認して下さい。CreateFile()の第6 引数の有無、ReadFile()の第5引数の有無によって、非同期か同期かが決定します。

非同期I/Oの場合に、ReadFile()の戻り値には注意が必要です。TRUEが返ってきた場合、読み込みの処理は 完了したことを表します。FALSEが返ってきた場合、エラーの可能性もありますが、非同期I/Oが完了していない 場合にもFALSEになります。ですから、本当にエラーかどうか判定するために、GetLastError()でエラーコードを 調べる必要があります。非同期I/Oが途中の状態の場合、ERROR_IO_PENDINGと いうコードが返されます。

非同期I/Oの完了待ちは、WinMain()のメッセージループ内に入れてしまいました。GetOverlappedResult() を毎回呼び出して、戻り値がTRUEになれば完了したことになります。FALSEが返って来た場合、先程同様、 本当にエラーなのかを調べる必要があります。GetLastError()がERROR_IO_INCOMPLETE を返した場合は、エラーではありません。また、ReadFile()の時点で、既に非同期I/Oが完了している可能性 があるため、その場合GetOverlappedResult()を呼び出すことがないように、それ自体をif文で囲っています。

実際のところ、この例のように毎回毎回、非同期I/Oの完了を調べることは良いことではないし、意味も ありません。読み込むデータが本当に必要になるタイミングでのみ確認すればいいはずです。

このサンプルでは、非常に巨大なファイルを読み込むことを前提としています。同期I/Oの場合、読み込み が終わるまでウィンドウが出てきません。読み込み処理で膨大な時間がかかり、ReadFile()から戻ってこない ためです。一方、非同期I/Oの場合、時間がかかるとしてもウィンドウはすぐに表示されます。ReadFile()から 戻ってこられるためです。




非同期I/Oを実装する方法はこれだけではありません。今回触れなかった、OVERLAPPED構造体のhEventの使い方に ついて次章で説明します。今回のサンプルで分かるように、非常に面倒なことが分かり ます。特に戻り値の確認作業は煩雑です。独自関数を作っておくと役に立つでしょう。


Win32API(C言語)編のトップページに戻る

サイトのトップページに戻る