EternalWindows
メッセージ管理 / スレッドとメッセージキュー

ウインドウを表示するアプリケーションが正しく動作するためには、 メッセージループの存在が必要不可欠です。 メッセージループは、GetMessageがメッセージキューからメッセージを取得し、 DispatchMessageでウインドウプロシージャにメッセージを送るのが目的ですが、 全てのウインドウへのメッセージが1つのメッセージループで処理されるとは限りません。 たとえば、別スレッドで作成されたウインドウに対するメッセージは、 メインスレッドのメッセージループで取得することができません。 本章は、こうした問題について考察するため、 メッセージキューをスレッドという角度から取り上げます。

スレッドは、コードの実行単位です。 つまり、実際にアプリケーションのコードを上から下へと処理していきます。 スレッドは複数存在することが可能で、 そのような場合はアプリケーションのコードが同時に実行されるようなことも起こります。 個々のスレッドは専用のデータ構造体を幾つか持っており、 その1つとしてメッセージキューがあります。 そして、このメッセージキューにはスレッドに関連付けられているウインドウへのメッセージがポストされることになります。

スレッドに関連付けられているウインドウとは、スレッドが作成したウインドウのことです。 CreateWindowExとGetMessageの呼び出しは1つのメインスレッドで行われていますから、 作成したウインドウ(子ウインドウ含む)へのメッセージはメインスレッドのメッセージキューに格納され、 メインスレッドからは問題なく取得することができます。 仮に、別スレッドがGetMessageを呼び出しても、 GetMessageはその別スレッドのメッセージキューからメッセージを取得するだけであるため、 メインスレッドのメッセージキューからメッセージを取得することはできません。 また、別スレッドによって作成されたウインドウへのメッセージは、 メインスレッドから取得することはできません。 なぜなら、そのメッセージは別スレッドのメッセージキューに格納されるからであり、 メインスレッドのGetMessageはこれを参照することができないからです。

ウインドウが1つのスレッドに関連付けていることは容易に証明することができます。 GetWindowThreadProcessIdという関数は、ウインドウハンドルから関連するスレッドIDを返すため、 これを特定のスレッドIDと比較すればよいことになります。 たとえば、GetWindowThreadProcessIdの戻り値がGetCurrentThreadIdの戻り値と一致したならば、 GetWindowThreadProcessIdに指定したウインドウは現在のスレッドによって作成されたことを意味しています。

GetMessageが別スレッドのメッセージキューを考慮していないことを証明するために、次のコードを用意しました。 実行すると分かるように、Thread Bというウインドウは一切の操作を受け付けません。

#include <windows.h>

BOOL MyCreateWindow(LPTSTR lpszAppName, HINSTANCE hinst, int nCmdShow);
DWORD WINAPI ThreadProc(LPVOID lpParameter);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	MSG    msg;
	DWORD  dwThreadId;
	HANDLE hThread;

	MyCreateWindow(TEXT("Thread A"), hinst, nCmdShow);
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, &dwThreadId);

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

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	MyCreateWindow(TEXT("Thread B"), GetModuleHandle(NULL), SW_SHOW);

	Sleep(INFINITE);

	return 0;
}

BOOL MyCreateWindow(LPTSTR lpszAppName, HINSTANCE hinst, int nCmdShow)
{
	HWND       hwnd;
	WNDCLASSEX wc;
	
	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = lpszAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return FALSE;

	hwnd = CreateWindowEx(0, lpszAppName, lpszAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return FALSE;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);

	return TRUE;
}

このアプリケーションは2つのウインドウを作成しますが、 実際に操作できるウインドウはThread Aというウインドウだけです。 Thread Bというウインドウは、CreateThreadによって作成された別スレッド内で作成されているため、 このウインドウへのメッセージはその別スレッドのメッセージキューに格納されます。 メインスレッドではGetMessageを呼び出していますが、 Thread Bというウインドウが動作しないことからも分かるように、 GetMessageは別スレッドのメッセージキューに格納されたメッセージを取得しません。 よって、Thread Bというウインドウを操作するためには、 ThreadProc内にもメッセージループを記述しなければなりません。

ThreadProcで呼び出しているSleepは、スレッドを直ちに終了させないために存在しています。 もし、スレッドが終了した場合に何が起きるかは、 先に述べたスレッドとウインドウの関係を考えれば容易に想像がつくでしょう。 ウインドウは1つのスレッドに関連付けられていますから、 その関連するスレッドが終了した場合は、スレッドによって作成されたウインドウも破棄されることになります。

GUIスレッドについて

今回はスレッドがメッセージキューを持っていることを説明しましたが、 このメッセージキューはスレッドが作成された時点で最初から存在しているわけではありません。 メッセージキューはあくまでスレッドが、user32.dllのウインドウやメッセージ関数を呼び出した場合に作成され、 このようにメッセージキューを持つスレッドがGUIスレッドと呼ばれます。 スレッドを強引にGUIスレッドにしたい場合は、IsGUIThreadにTRUEを指定するか次のコードを実行します。

MSG msg;

PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

これにより、スレッドはGUIスレッドになります。 実際にスレッドがGUIスレッドかどうかを調べたい場合は、 IsGUIThreadにFALSEを指定し、戻り値が0でないかを確認するとよいでしょう。 ちなみに、GUIスレッドの情報を取得するGetGUIThreadInfoは、 スレッドがGUIスレッドでなくても成功するので注意してください。



戻る