スレッドの同期
クリティカルセクション
マルチスレッドプログラミングは、プログラムの全体の分野でも高度な手法です
プログラムをする上で、本当に必要な場合を除けば使うことを避けるべきでもあります
同じことを、タイマーで実現できるのならば、タイマーを使うべきであるとも言えます
なぜ、スレッドは高度で危険を含むのか
それは、同じ記憶領域を複数のプログラム(スレッド)が操作するからです
一つのグローバル変数を、二つのスレッドが同時に参照することを考えてください
お互い、この変数を書き換えたり読み込んだりして、重要な処理を行う時
正しい順番で読み書きが行われなかったとき、プログラムの整合性は失われます
#include <windows.h>
int iCount;
DWORD WINAPI ThreadFunc1(LPVOID hWnd) {
HDC hdc;
TCHAR str[32];
for (iCount = 0 ; iCount < 1000 ; iCount++) {
hdc = GetDC(hWnd);
wsprintf(str , TEXT("Count = %d") , iCount);
TextOut(hdc , 0 , 0 , str , lstrlen(str));
ReleaseDC(hWnd , hdc);
Sleep(10);
}
ExitThread(TRUE);
}
DWORD WINAPI ThreadFunc2(LPVOID hWnd) {
HDC hdc;
TCHAR str[32];
for (iCount = 0 ; iCount > -1000 ; iCount--) {
hdc = GetDC(hWnd);
wsprintf(str , TEXT("Count = %d") , iCount);
TextOut(hdc , 0 , 20 , str , lstrlen(str));
ReleaseDC(hWnd , hdc);
Sleep(10);
}
ExitThread(TRUE);
}
LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
DWORD dwParam;
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_CREATE:
CreateThread(
NULL , 0 , ThreadFunc1 , hWnd , 0 , &dwParam);
CreateThread(
NULL , 0 , ThreadFunc2 , hWnd , 0 , &dwParam);
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;
}
例えば、このプログラムは、二つのスレッドが同時に iCount グローバル変数を参照します
一方のスレッドは iCount をインクリメントしつづけ、もう一方はデクリメントしつづけます
二つのスレッドは優先順位が同じなので、変数の値は一定の均衡を保ちます
そのため、二つのスレッドはループから脱することができず、永遠と処理をしつづけます
この例は、それを想定して書かれているので、見ただけで競合していることがわかりますが
プログラムによっては、競合による例外が極、稀にしか発生しないこともあります
そのような場合、バグを追跡するのは困難な作業となるでしょう
しかし、プログラムによっては、スレッドが同一の記憶領域を
同時に読み書きするという作業は、非常に重要かつ必須であることがあります
このような場合は、同期をとることによって問題を解決します
スレッドの同期とは、複数のスレッド全体を監視し、制御することで
簡単に表現するならば、スレッドの流れに信号機をつけるようなものと考えてください
あるスレッドが処理中の場合、他のスレッドは待機するようにしかけます
(もちろん、この概念はプロセスにも同様に当てはまります)
これを実現するには、クリティカルセクションを使います
クリティカルセクションとは、整合性を保つためにスレッドの処理を切り替えない時間を指します
クリティカルセクションの間は、他のスレッドは処理中のスレッドの処理終了を待ちます
Win32 API でこの同期を行うには CRITICAL_SECTION 構造体を使います
この構造体は、クリティカルセクションオブジェクトとなる重要な構造体で
クリティカルセクションに関連した関数に渡すために使います
ただし、この構造体については完全にブラックボックス扱いしなければなりません
すなわち、アプリケーションは構造体のメンバにアクセスしてもいけませんし
当然、メンバにデータを書き込んでも、構造体をコピーしてもいけません
構造体の初期化は InitializeCriticalSection() が行います
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
lpCriticalSectioni にはクリティカルセクションオブジェクトへのポインタを指定します
とにかく、これらの処理は Windows によってブラックボックス化されていますので
私たちは、この構造体や関数が何をしているのかは考える必要がありません
作成したクリティカルセクションオブジェクトは、不必要になれば破棄します
クリティカルセクションオブジェクトの破棄は DeleteCriticalSection() を使います
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
lpCriticalSection には、破棄するクリティカルセクションオブジェクトへのポインタを指定します
これで、破棄したクリティカルセクションオブジェクトは、もう使えません
オブジェクトが使用していたシステムリソースは全て解放されます
次に、作成したクリティカルセクションオブジェクトを使って、スレッドを制御します
スレッドがクリティカルセクションに入るには EnterCriticalSection() を呼び出します
これで、この関数を呼び出したスレッドがクリティカルセクションに入ります
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
lpCriticalSection には、クリティカルセクションオブジェクトへのポインタを指定します
クリティカルセクションオブジェクトは、一つの鍵を持っていると想定してください
この鍵を持つスレッドが、クリティカルセクションを実行する権利を得られます
鍵は1つしか無いので、後からこの関数を実行したスレッドに鍵はありません
クリティカルセクションを実行中のスレッドが処理を終了し、鍵を開放するまで
後続してこの関数を呼び出したスレッドは待機しなければなりません
スレッドは、クリティカルセクションの処理を終了したならば、鍵を返さなければなりません
そうでなければ、後続のスレッドがクリティカルセクションを実行できないのです
これには LeaveCriticalSection() 関数を使います
これを使えば、鍵、つまりクリティカルセクションオブジェクトの所有権を解放するのです
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
lpCriticalSection には、クリティカルセクションオブジェクトへのポインタを指定します
これを実行すれば、スレッドはクリティカルセクションから抜け出します
異なるスレッドが待機中であれば、今度はそれが即座にクリティカルセクションに入るでしょう
これを使えば、先ほどのようなプログラムを安全に動かすことができます
グローバル変数 iCount をスレッドが操作する時は、クリティカルセクションオブジェクトを所有し
処理が終了すれば、所有権を解放します
このようにすれば、二つのスレッドが競合しあうようなことはなくなります
#include <windows.h>
typedef struct {
HWND hWnd;
CRITICAL_SECTION cs;
} THREADPARAM;
int iCount;
DWORD WINAPI ThreadFunc1(LPVOID pvParam) {
HDC hdc;
TCHAR str[32];
THREADPARAM * tp = (THREADPARAM *)pvParam;
EnterCriticalSection(&tp->cs);
for (iCount = 0 ; iCount < 1000 ; iCount++) {
hdc = GetDC(tp->hWnd);
wsprintf(str , TEXT("Count = %d") , iCount);
TextOut(hdc , 0 , 0 , str , lstrlen(str));
ReleaseDC(tp->hWnd , hdc);
Sleep(10);
}
LeaveCriticalSection(&tp->cs);
ExitThread(TRUE);
}
DWORD WINAPI ThreadFunc2(LPVOID pvParam) {
HDC hdc;
TCHAR str[32];
THREADPARAM * tp = (THREADPARAM *)pvParam;
EnterCriticalSection(&tp->cs);
for (iCount = 0 ; iCount > -1000 ; iCount--) {
hdc = GetDC(tp->hWnd);
wsprintf(str , TEXT("Count = %d") , iCount);
TextOut(hdc , 0 , 20 , str , lstrlen(str));
ReleaseDC(tp->hWnd , hdc);
Sleep(10);
}
LeaveCriticalSection(&tp->cs);
ExitThread(TRUE);
}
LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
DWORD dwParam;
static THREADPARAM tp;
switch (msg) {
case WM_DESTROY:
DeleteCriticalSection(&tp.cs);
PostQuitMessage(0);
return 0;
case WM_CREATE:
tp.hWnd = hWnd;
InitializeCriticalSection(&tp.cs);
CreateThread(
NULL , 0 , ThreadFunc1 , &tp , 0 , &dwParam);
CreateThread(
NULL , 0 , ThreadFunc2 , &tp , 0 , &dwParam);
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;
}
これは、最初のプログラムを書き換え、クリティカルセクションを使った例です
スレッドは、即座にクリティカルセクションに入り、そのなかでループを繰り返します
後に続いたスレッドは、所有権が自分に移るのを待たなければなりません
同一のメモリや、ファイルなどの記憶領域を異なるスレッドが共有する場合
プログラムの整合性のために、できる限り細心の注意をはらう必要があります
必要でない限り、スレッドが記憶領域を共有してはいけません。それが理想的です
InitializeCriticalSection()
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
クリティカルセクションオブジェクトを初期化します
lpCriticalSectioni - クリティカルセクションオブジェクトへのポインタを指定します
DeleteCriticalSection()
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
クリティカルセクションオブジェクトを解放します
lpCriticalSectioni - クリティカルセクションオブジェクトへのポインタを指定します
EnterCriticalSection()
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
クリティカルセクションオブジェクトの所有権が得られるまで待機します
所有権が得られると、関数は制御を返します
lpCriticalSectioni - クリティカルセクションオブジェクトへのポインタを指定します
LeaveCriticalSection()
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
クリティカルセクションオブジェクトの所有権を解放します
lpCriticalSectioni - クリティカルセクションオブジェクトへのポインタを指定します