第87章 マルチスレッド その1


今回から、マルチスレッドのプログラミングの解説をします。 これは、ウィンドウズが32ビット版になって実現した新機能です。 そもそもマルチスレッドとは何なのでしょうか?

正確な回答は大変難しいのですが、気持ち的には一つの プログラム中に複数のスレッドを走らせることのできる しくみと考えてください。スレッドは一つのプログラムみたいなもので これはこれで勝手に動作します。あれこれ考えるより 例題を見た方が早いでしょう。今回のサンプルは親ウィンドウと子供ウィンドウ を作りそれぞれに丸と四角形をランダムに表示するというものです。


左のようなプログラムはどのように作ればよいのでしょうか。
まず、親と子供ウィンドウを作りSetTimer関数を使って、 WM_TIMERメッセージが来る度にランダムに丸やら四角を描画する。 もちろんそれでも作れます。しかし、今回は丸と四角のランダム描画の ためのスレッドを作ってスレッド上で動かします。
スレッドを作るための準備としては

1.コンパイラの設定でコード生成をマルチスレッドにします 2.Libcmt.lib, Msvcrt.libをリンクします 3.ソースファイルにprocess.hをインクルードします

これは、_beginthread関数を使うための準備です。
コンパイラの設定はVC++5.0では「プロジェクト」「設定」「C/C++」「コード生成」 の「使用するランタイムライブラリ」を「マルチスレッド」にします。

// mult01.cpp #define STRICT #include <windows.h> #include <process.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK MyChProc(HWND, UINT, WPARAM, LPARAM); BOOL InitApp(HINSTANCE); BOOL InitInstance(HINSTANCE, int); void Thread(void *); void ThreadMain(void *); typedef struct { HWND hwnd; int end; } VAL, *PVAL; char szClassName[] = "mult01"; //ウィンドウクラス HINSTANCE hInst; int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }

プロトタイプ宣言されているThreadとか、ThreadMainという 関数が今回作られたスレッドです。必ずvoidを返す関数で 引数は(void *)です(PVOIDとしても同じです)。

次にVALとかPVALという構造体やら、構造体へのポインタ をtypedefしています。これは、筆者が勝手に作った ものでスレッドに渡す引数を格納します。

WinMain関数はいつもと同じです。

//ウィンドウ・クラスの登録 BOOL InitApp(HINSTANCE hInst) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; //プロシージャ名 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; //インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; //メニュー名 wc.lpszClassName = (LPCSTR)szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); return (RegisterClassEx(&wc)); }

ここはいつもと同じです。

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { HWND hWnd; hInst = hInstance; // インスタンスハンドルの保存 hWnd = CreateWindow(szClassName, "猫でもわかるマルチスレッド",//タイトルバーにこの名前が表示されます WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, //ウィンドウの種類 CW_USEDEFAULT, //X座標 CW_USEDEFAULT, //Y座標 CW_USEDEFAULT, //幅 CW_USEDEFAULT, //高さ NULL, //親ウィンドウのハンドル、親を作るときはNULL NULL, //メニューハンドル、クラスメニューを使うときはNULL hInst, //インスタンスハンドル NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; }

ここも、いつもとほとんど同じです。しかし、ウィンドウスタイル に必ずWS_CLIPCHILDRENを加えて置いてください。これがないと子供ウィンドウの 上に丸が書き込まれてしまいます。

//ウィンドウプロシージャ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { int id; static char *szChildName = "child"; static HWND hChild; static VAL val; WNDCLASSEX wc; RECT rc; switch (msg) { case WM_CREATE: wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; //インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = NULL; //メニュー名 wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); wc.lpfnWndProc = MyChProc; wc.lpszClassName = szChildName; RegisterClassEx(&wc); hChild = CreateWindow(szChildName, "子供", WS_CHILD | WS_VISIBLE | WS_THICKFRAME | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, 0, 0, 0, 0, hWnd, (HMENU)100, hInst, NULL); val.hwnd = hWnd; val.end = 0; _beginthread(ThreadMain, 0, &val); break; case WM_SIZE: GetClientRect(hWnd, &rc); MoveWindow(hChild, (rc.right - rc.left) / 2, 0, (rc.right - rc.left) / 2, rc.bottom - rc.top, TRUE); break; case WM_RBUTTONDOWN: val.end = 1; break; case WM_CLOSE: id = MessageBox(hWnd, (LPCSTR)"終了してもよいですか", (LPCSTR)"終了確認", MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { if (IsWindow(hChild)) DestroyWindow(hChild); val.end = 1; Sleep(1000); DestroyWindow(hWnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0L; }

親ウィンドウのプロシージャを順番に見ていきましょう。

WM_CREATEが来たら子供ウィンドウを作ります。そしてval.endに0 を設定します。また、val.hwndに親のウィンドウハンドルを指定します。 この情報は、親ウィンドウにランダムに丸を描画するスレッドに送られます。 次に_beginthread関数を呼び出してスレッドを開始します。 この関数の最初の引数は開始されるスレッド関数のアドレスです。 2番目の引数は新しいスレッドのスタックサイズを指定します。 0にするとメインスレッドと同じサイズになります。 最後の引数はスレッドに渡されるデータのアドレスです。

WM_SIZEメッセージが来たとき、すなわち親ウィンドウのサイズが 変更になったときは子供ウィンドウは親のクライアント領域の 右半分に来るようにしてみました。(別に深い意味はありません)

WM_RBUTTONDOWNメッセージが来たとき、つまり親のクライアント領域 を右クリックしたときval.endに1をセットして親に描画している スレッドを終了させます。スレッドがきちんと終了するかのテスト用です。

親ウィンドウの右上にある×印などが押されたときに
子供ウィンドウが存在するときはこれを終了させます。 また、親に描画していたスレッドを終了させるために val.endに1をセットします。次に1秒ほど眠らせます。

VOID Sleep( DWORD dwMilliseconds //ミリセカンド );

これは、現在のスレッドを指定した時間だけ停止します。 どうしてこんなことをするかというと苦肉の策です。 本来のプログラム(これもスレッドの1つです。)、 親に描画するスレッド、子に描画するスレッドが動いているとき 親の右上の×印を押したとします。すると、本来のプログラムは 子供と親のウィンドウをクローズしてプログラムが終了します。 ×印が押されたときval.endに1を設定してスレッドを終了するように しましたがスレッドが終了するよりも先にプログラム本体が 終了してしまう可能性があります。_beginthreadで作ったスレッドでは スレッドが終了するときにhdcの解放を行っているのでプログラム自体が 終了するときにはスレッドがきちんと終了しているのが 望ましいと思われます。

void ThreadMain(void *data) { static PVAL pval; HDC hdc; RECT rc; int wx, wy, x, y; pval = (PVAL)data; hdc = GetDC(pval->hwnd); while (pval->end == 0) { GetClientRect(pval->hwnd, &rc); wx = rc.right - rc.left; wy = rc.bottom - rc.top; if (wx == 0) wx = 1; if (wy == 0) wy = 1; x = rand() % wx; y = rand() % wy; Ellipse(hdc, x, y, x + 30, y + 30); } if(ReleaseDC(pval->hwnd, hdc) != 0) MessageBox(pval->hwnd, "hdcを解放しました", "ReleaseDC", MB_OK); _endthread(); return; }

親ウィンドウに描画をするスレッド関数です。戻り値はvoidで引数は(void *) となります。この関数は親のプロシージャで開始されたスレッドです。 親のプロシージャでセットされたval構造体のデータはここにやってきます。 pval->endの値が0の時はwhileループがぐるぐる回って親ウィンドウに 丸を描画します。丸の位置を指定するx, yは乱数で作ります。
pval->endが1になると、ループを抜けます。hdcを解放して_endthread関数で スレッドを終了します。

LRESULT CALLBACK MyChProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { static VAL val; switch (msg) { case WM_RBUTTONDOWN: val.end = 1; return 0; case WM_CREATE: val.hwnd = hWnd; val.end = 0; _beginthread(Thread, 0, &val); return 0L; case WM_DESTROY: val.end = 1; Sleep(1000); return 0L; } return DefWindowProc(hWnd, msg, wp, lp); }

子供ウィンドウのプロシージャです。

子供ウィンドウもクライアント領域を右クリックするとval.endに1をセットして 子供ウィンドウに描画しているスレッドを終了させるようにしました。 これも、スレッドにきちんと情報が伝わるかどうかのテスト用です。

WM_CREATEメッセージが来たら(子供ウィンドウが作られたらすぐに) val.hwndに子供ウィンドウのハンドルをセットし、val.endに0をセットします。 そして、_beginthread関数でスレッドを開始します。

子供ウィンドウが破壊されるときにもval.endに1をセットしてスレッドを 終了させます。このときも1秒ほど眠らせてスレッドを終了させる時間を 作ります。

void Thread(void *data) { static PVAL pval; static RECT rc; int wx, wy, x, y; HDC hdc; pval = (PVAL)data; hdc = GetDC(pval->hwnd); GetClientRect(pval->hwnd, &rc); while (pval->end == 0) { GetClientRect(pval->hwnd, &rc); wx = rc.right - rc.left; if (wx == 0) wx = 1; wy = rc.bottom - rc.top; if (wy == 0) wy = 1; x = rand() % wx; y = rand() % wy; Rectangle(hdc, x, y, x+20, y+20); } if (ReleaseDC(pval->hwnd, hdc) != 0) MessageBox(NULL, "hdcを解放", "OK", MB_OK); _endthread(); return; }

子供ウィンドウに四角形を描画するスレッドです。データのもらい方は 親に描画するスレッドと同じです。 whileループを抜けると(pval->endに1がセットされた)hdcを解放して スレッドを終了します。

さて、だいたいの雰囲気がわかったでしょうか。スレッドに渡すデータ はひとつもグローバル変数を使っていないことに注意してください。 もちろん、グローバル変数を複数のスレッドから参照したり書き換えたり する事もできます。しかしその場合マルチスレッド特有の面倒な問題が 起こります。次章からは少しいろいろな場合について考えてみることにします。


[SDK Index] [総合Index] [Previous Chapter] [Next Chapter]

Update Nov/09/1997 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。