マルチスレッド


並列処理

Windows オペレーティングシステムは、プリエンプティブなマルチタスクをサポートしています
マルチタスクとは、CPU が連続的に実行するべきプログラムを切り替えることによって
まるで、同時に複数のプログラムを実行させているように見せかける技術のことです
これは Windows に特化したテクノロジではないので、この場で説明はしません
詳しくは情報技術全般の知識を身につけてください

タスクは CPU に与えられている仕事(ジョブ)をプログラムから見たもので
UNIX では プロセス と呼ばれることもあります

私たちがこれまで作って実行してきたプログラムも
Windows から見れば、たくさんあるプロセスの一つと考えられます
Windows はプログラムの機動を要求されると、プロセスを作成して実行するのです

プロセスには、驚くほど高度で難解な技術が使われています
複数のプロセスをタイムスライスに割り当て切り替え、必要になれば復帰させる
この時、メモリや実行アドレスを少しでも間違えれば、プログラムは正常に動きません
しかし、これは OS が管理することであり、私たちはカーネルの仕事に口出しはできません

さらに、Windows はマルチスレッドをサポートしています
スレッドとはプロセスが保有するジョブと位置付けることができます
つまり、マルチスレッドとはプロセスがマルチタスクを実現する手段です
マルチスレッドを使えば、一つのプログラムが並行処理を行うことができるようになります

マルチタスクとマルチスレッドの違いは、メモリにあります
マルチタスク上のそれぞれのプロセスは、それぞれ独立した論理メモリ空間を持ちます
私が作ったソフトウェアとあなたが作ったソフトウェアが同時に起動した時
データを保存するメモリ空間の一部が共有されてしまったら、それは大変なことになります

これに対し、マルチスレッドは親プロセスのメモリ空間を共有します
スレッドは確実に一つのプログラムの下で動くので、それでも問題がないのです

マルチスレッドのうち、WinMain() 関数が実行しているスレッドを主スレッドを呼びます
主スレッドは、プライマリスレッドとも呼ばれ、OS が作成します
このスレッドは、メッセージループを作成し、プロシージャに送信する役割を持ちます
以前、何度も言いましたが、主スレッドはプロシージャから
即座にメッセージループに復帰しなければなりません

主スレッドが、ウィンドウプロシージャ内で無限ループに陥ると
ウィンドウは他のメッセージを処理できなくなり、フリーズしてしまいます

主スレッドは、国政で例えるならば外務大臣の位置にあります
外務大臣が内部の問題で外交を上手く果たせなければ、罷免を食らいます
外務大臣は、ユーザーを相手に入出力を処理しなければなりません
そして、実際の仕事は外務省の役人に任せるということが、もっとも適切なプログラムです
ここで、「外務省の役人」となるのが、副スレッドです

主スレッドは、必要に応じて副スレッドを作成して実行することができます
スレッドを作成するには CreateThread() 関数を使います
HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES lpThreadAttributes,
	DWORD dwStackSize, 
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	DWORD dwCreationFlags,
	LPDWORD lpThreadId
);
lpThreadAttributes には、セキュリティ属性構造体へのポインタを指定します
この構造体を使うのは Windows NT のファイルシステムの場合です
NULL を指定すればデフォルトのセキュリティが使われます
この場は、9x 系のファイルシステムを想定しているので、NULL とします

dwStackSize は、新しいスレッドが保有するスタックサイズを指定します
0 を指定すればデフォルト(主スレッドと同じサイズ)で割り当てられます
スタックのサイズは、必要に応じて大きくなります

lpStartAddres には、作成されたスレッドの実行開始アドレスを指定します
これは、概念上コールバック関数とは異なるものなので注意してください
lpParameter は、スレッドに渡す追加情報を指定します

dwCreationFlags はスレッドの状態を表すフラグを指定できます
0 を指定すれば、スレッドは作成された直後に実行可能状態となりますが
CREATE_SUSPENDED を指定すれば、待機状態で作成されます
これをサスペンド状態といって、実行する方法は後ほど説明します

lpThreadId には、スレッドの ID を格納するための DWORD 型変数へのポインタを指定します
関数が成功すれば、作成したスレッドのハンドル、失敗すれば NULL が返ります

lpStartAddres 引数に渡す実行開始アドレスのポインタは
LPTHREAD_START_ROUTINE 型で、次の関数型へのポインタです

DWORD WINAPI ThreadProc(LPVOID lpParameter);

lpParameter には、主スレッドからの追加情報が格納されています
戻り値は、成功、または失敗を表す情報を返すことができます

CreateThread() 関数はスレッドのハンドルを返してきます
スレッドに対して特別な制御を行わない場合、特に必要なものではありません
ただし、処理を終了してもスレッドはシステムに残ります
スレットオブジェクトを完全に解除するには、CloseHandle() にスレッドのハンドルを渡します
#include <windows.h>

DWORD WINAPI ThreadFunc(LPVOID vdParam) {
	HDC hdc;
	unsigned int iCount = 0;
	TCHAR strCount[128];

	while (TRUE) {
		hdc = GetDC((HWND)vdParam);

		InvalidateRect((HWND)vdParam , NULL , TRUE);
		wsprintf(strCount , "Count = %d" , iCount);
		TextOut(hdc , 10 , 10 , strCount , lstrlen(strCount));

		ReleaseDC((HWND)vdParam , hdc);
		iCount = iCount == 0xFFFFFFFF ? 0 : iCount + 1;
	}
}

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	DWORD dwID;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		CreateThread(NULL , 0 , ThreadFunc , (LPVOID)hWnd , 0 , &dwID);
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
このプログラムを実行すると、0 から順にカウントする文字が表示されます
この処理は、CPU の能力をできる限りフルに使っているにもかかわらず
主スレッドはメッセージループを監視しているので、入出力は制御されています

カウンタと描画処理は、副スレッドで行っています
タイマーと異なり、CPU の余裕がある限り高速でカウントが進んでいくでしょう
Ghz コンピュータの場合、速すぎて何が書かれているのかわからないかも知れません
メモリデバイスコンテキストを用意して、そこにカウントを描画し
最後に BitBlt() などでメモリデバイスコンテキストを描画すれば、ちらつきがなくなります


スレッドの終了

スレッドを終了する時は、終了用の関数を使います
CreateThread() 関数で作成したスレッドは ExitThread() で終わります

VOID ExitThread(DWORD dwExitCode);

dwExtiCode には、このスレッドの終了コードを指定します
この関数を呼び出すと、この関数を呼び出したスレッドが終了します
呼び出したスレッドが、プロセスの最後のスレッドであれば、そのプロセスも終了します

終了コードを取得するには GetExitCodeThread() 関数を使います
この関数は、スレッドが生きているかどうかを調べることも可能です

BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode ); hThread には、対象のスレッドを指定します
lpExitCode には、終了コードを受け取る DWORD 型へのポインタを指定します
成功すれば 0 以外、失敗すれば 0 が返ります

ただし、明示的に ExitThread() 関数を呼び出さなくても
スレッド関数が終了すれば、自動的に ExitThread() が呼び出される仕組みになっています
そのため、一般的には ExitThread() を明示的に呼び出すことはありません
#include <windows.h>

DWORD WINAPI ThreadFunc(LPVOID hWnd) {
	HDC hdc;
	int iRed = 0 , iX = 0 , iY = 0;
	RECT rect;

	while (TRUE) {
		hdc = GetDC(hWnd);
		GetClientRect(hWnd , &rect);
		SetPixel(hdc , iX , iY , RGB(iRed , 0 , 0));
		ReleaseDC(hWnd , hdc);

		if (iX < rect.right) iX++;
		else if (iY < rect.bottom) {
			iX = 0; iY++;
		}
		else break;
		iRed = iRed == 255 ? 0 : iRed + 1;
	}
	ExitThread(TRUE);
}

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	DWORD dwParam;
	TCHAR str[128];
	static HANDLE hThread;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		hThread = CreateThread(
			NULL , 0 , ThreadFunc , hWnd , 0 , &dwParam);
		return 0;
	case WM_RBUTTONUP:
		GetExitCodeThread(hThread , &dwParam);
		if (dwParam == STILL_ACTIVE)
			MessageBox(hWnd , TEXT("Thread is still alive") ,
				TEXT("スレッドは生きています") , MB_OK);
		else {
			wsprintf(str , TEXT("Code = %d") , dwParam);
			MessageBox(hWnd , str , TEXT("コード") , MB_OK);
		}
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
このプログラムは、副スレッドがウィンドウの左上隅から右下隅まで
1ピクセルずつ SetPixel() 関数で点を打ち続けます
点の色は、黒から赤になって、赤になると再び黒に戻ります
そして、ウィンドウの右下隅まで描画すれば、スレッドは終了します

ウィンドウのクライアント領域で右クリックすると、ダイアログが表示されます
スレッドが実行中であれば、実行中であるというメッセージを表示ますが
終了していれば、スレッドの終了コードを表示します
このように、GetExitCodeThread() はスレッドが実行中かどうかの判断にも使えます

推奨できませんが、最悪の場合 TerminateThread() を使うこともできます
これは、スレッドに何らかの異常が発生し、強制的に終了する時に使います

BOOL TerminateThread(HANDLE hThread , DWORD dwExitCode);

hThread には強制終了させるスレッドのハンドルを
dwExtiCode には終了コードを指定します
関数が成功すれば 0 以外、失敗すれば 0 が返ります

この関数を呼び出す時は、スレッドが何をしているのかを完全に把握している時だけです
スレッドの状態にかまわず、外部から強制終了する行為は、本来望まれません
スタックの解放なども行われませんし、メモリリークを起こす危険性もあります
場合によっては、プログラムの整合性を破壊する可能性があるので注意が必要です
#include <windows.h>

DWORD WINAPI ThreadFunc(LPVOID hWnd) {
	HDC hdc;
	int iRed = 0 , iX = 0 , iY = 0;
	RECT rect;

	while (TRUE) {
		hdc = GetDC(hWnd);
		GetClientRect(hWnd , &rect);
		SetPixel(hdc , iX , iY , RGB(iRed , 0 , 0));
		ReleaseDC(hWnd , hdc);

		if (iX < rect.right) iX++;
		else if (iY < rect.bottom) {
			iX = 0; iY++;
		}
		else break;
		iRed = iRed == 255 ? 0 : iRed + 1;
	}
	ExitThread(TRUE);
}

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	DWORD dwParam;
	static HANDLE hThread;

	switch (msg) {
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	case WM_CREATE:
		hThread = CreateThread(
			NULL , 0 , ThreadFunc , hWnd , 0 , &dwParam);
		return 0;
	case WM_RBUTTONUP:
		if (!hThread) return 0;

		GetExitCodeThread(hThread , &dwParam);
		if (dwParam == STILL_ACTIVE) {
			TerminateThread(hThread , FALSE);
			CloseHandle(hThread);
		}
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc	= WndProc;
	winc.cbClsExtra	= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
このプログラムは、先ほどのプログラムの一部を改良したものです
スレッドが実行中の時に、右クリックすると
主スレッドは副スレッドを強制終了させます


CreateThread()

HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES lpThreadAttributes,
	DWORD dwStackSize, 
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	DWORD dwCreationFlags,
	LPDWORD lpThreadId
);
新しいスレッドを作成します

lpThreadAttributes - セキュリティ属性構造体へのポインタを指定します
dwStackSize - 新しいスレッドが保有するスタックサイズを指定します
lpStartAddres - 作成されたスレッドの実行開始アドレスを指定します
lpParameter - スレッドに渡す追加情報を指定します
dwCreationFlags - スレッドの状態を表すフラグを指定できます
lpThreadId - ID を格納するための DWORD 型変数へのポインタを指定します

戻り値 - 作成したスレッドのハンドル、失敗すれば NULL

dwCreationFlags には以下の定数を渡すことができます

定数意味
0 スレッドを作成後、即座に実行させます
CREATE_SUSPENDED 新しいスレッドをサスペンド状態で作成します
スレッドを実行するには、ResumeThread() 関数を使います

ThreadProc()

DWORD WINAPI ThreadProc(LPVOID lpParameter);

アプリケーション定義の新しいスレッドのエントリポイント関数です

lpParameter - 追加情報を指定します

戻り値 - 成功、または失敗を表す終了コード

ExitThread()

VOID ExitThread(DWORD dwExitCode);

この関数を呼び出したスレッドを終了させます

dwExtiCode - このスレッドの終了コードを指定します

GetExitCodeThread()

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);

スレッドの終了コードを取得します
スレッドが実行中であるかを調べることもできます

hThread - 対象のスレッドを指定します
lpExitCode - 終了コードを受け取る DWORD 型へのポインタを指定します

戻り値 - 成功すれば 0 以外、失敗すれば 0

TerminateThread()

BOOL TerminateThread(HANDLE hThread , DWORD dwExitCode);

スレッドを強制終了させます

hThread - 強制終了させるスレッドのハンドルを指定します
dwExtiCode - 終了コードを指定します

戻り値 - 成功すれば 0 以外、失敗すれば 0



前のページへ戻る次のページへ