イベント
排他制御や同期制御の方法としてクリティカルセクション、ミューテックスやセマフォなどを説明しました。
これらはオブジェクトの所有権やリソース数などで状態(シグナル状態/ノンシグナル状態)を変化させますが、もっとシンプルなものにイベント
オブジェクトがあります。
イベントオブジェクトの状態は任意のスレッド(プロセス)から任意のタイミングで、特別な条件なしに設定できます。
イベント関数
イベント関数の使い方はミューテックス関数やセマフォ関数と基本的に同じです。
スレッドの待機にはWaitForSingleObject関数などの待機関数を使用します。
CreateEvent関数
イベントオブジェクトはCreateEvent
関数で作成します。
- HANDLE CreateEventW(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCWSTR lpName
); - イベントオブジェクトを作成する。
または既存のイベントオブジェクトを開く。
- lpEventAttributes
-
イベントオブジェクトのセキュリティ記述子です。
NULL
を指定するとハンドルの子プロセスへの継承は禁止されます。
通常はNULLで構いません。 - bManualReset
-
TRUE
を指定すると手動イベントオブジェクトを作成します。
FALSE
を指定すると自動イベントオブジェクトを作成します。手動イベントオブジェクトは、シグナル状態をノンシグナル状態に変更するために後述する
ResetEvent
を実行する必要があります。
自動イベントオブジェクトは待機関数によりスレッドの待機が解放されると(つまりシグナル状態が取得されると)自動的にノンシグナル状態に変更されます。 - bInitialState
-
イベントオブジェクトの初期状態の指定です。
TRUE
を指定すると初期状態はシグナル状態です。
FALSE
を指定すると初期状態はノンシグナル状態です。 - lpName
-
イベントジェクトの名前を指定します。
この名前のイベントオブジェクトがシステムに存在しない場合は作成され、システムに登録されます。
すでに存在する場合はEVENT_ALL_ACCESS
というアクセス権を要求します。
この場合、引数bManualReset
とbInitialState
はすでに設定済みであるため無視されます。
引数lpEventAttributes
のbInheritHandle
メンバにより、ハンドルの子プロセスへの継承を設定することは可能ですが、セキュリティ記述子は無視されます。名前は大文字小文字を区別します。
長さは最大でMAX_PATH
まで指定可能です。
既存のミューテックス、セマフォ、待機可能タイマー、ジョブ、ファイルマッピングオブジェクトの名前と重複すると関数は失敗します。
NULL
を指定すると名前なしイベントを作成します。 - 戻り値
-
関数が成功した場合はイベントジェクトのハンドルを返します。
指定した名前のイベントジェクトが既に存在する場合も関数は成功しますが、GetLastError
関数はERROR_ALREADY_EXISTS
を返します。
関数が失敗した場合はNULL
を返します。
イベントオブジェクトは、不要になったらCloseHandle関数でハンドルを閉じる必要があります。
イベントオブジェクトは複数のスレッド/プロセスから参照することができ、CloseHandle関数によってひとつも参照するスレッド/プロセスがなくなった場合にシステムから削除されます。
(明示的にCloseHandle関数を呼ばなくてもアプリケーションが終了した時にハンドルは解放されます)
OpenEvent関数
システム上に存在するイベントオブジェクトのハンドルを取得するにはOpenEvent
関数を使用します。
- HANDLE OpenEventW(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCWSTR lpName
); - 既存のイベントオブジェクトlpNameのハンドルを取得する。
失敗した場合はNULLを返す。
- dwDesiredAccess
-
イベントオブジェクトに要求するアクセス権の指定です。
イベントオブジェクトは同期のためのSYNCHRONIZE
フラグと、状態変更の許可のためのEVENT_MODIFY_STATE
フラグの両方を指定します。
セキュリティを変更する場合はMUTEX_ALL_ACCESS
フラグを指定します。
このフラグはSYNCHRONIZE
フラグやEVENT_MODIFY_STATE
フラグを含む全ての権限を要求します。//通常の範疇での使用の場合 HANDLE hEvent = OpenEvent( SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, eventName);
- bInheritHandle
-
取得したハンドルを子プロセスに継承できるか否かを
TRUE
/FALSE
で指定します。 - lpName
-
開くイベント名を指定します。
名前なしイベントは取得できません。 - 戻り値
-
成功した場合はイベントオブジェクトのハンドルです。
失敗した場合はNULL
を返し、GetLastError
関数はERROR_FILE_NOT_FOUND
を返します。
SetEvent関数
イベントオブジェクトをシグナル状態にするにはSetEvent
関数を使用します。
- BOOL SetEvent(
HANDLE hEvent
); - イベントオブジェクトハンドルhEventをシグナル状態にする。
成功した場合は0以外を、失敗した場合は0を返す。
ResetEvent関数
イベントオブジェクトをノンシグナル状態にするにはResetEvent
関数を使用します。
- BOOL ResetEvent(
HANDLE hEvent
); - イベントオブジェクトハンドルhEventをノンシグナル状態にする。
成功した場合は0以外を、失敗した場合は0を返す。
ノンシグナル状態のイベントはSetEvent関数を実行するまでノンシグナル状態のままです。
(その他にもPulseEvent
という関数がありますが、これは非推奨関数です)
自動イベントオブジェクトの場合、待機関数によってシグナル状態が取得されると自動的にノンシグナル状態に変更されます。
手動イベントオブジェクトの場合は自動的には変更されず、ノンシグナル状態に変更するためにResetEvent関数を実行します。
サンプルコード
イベントを利用して、複数のスレッドの初期化と実行タイミングをそろえるサンプルコードです。
#define _CRT_RAND_S
//↑rand_s関数を使用するために必要
#include <windows.h>
#include <strsafe.h>
#include <process.h>
//ウィンドウの生成等は省略
#define BUFFERSIZE 16
#define IDC_BUTTON1 100
typedef struct {
HANDLE hEventThreadInitialized;
HANDLE hEventThreadAllInitialized;
HANDLE hEventPause;
//以下のメンバはスレッド毎に異なる値をセットする
HWND hStatic;
int add;
} ThreadParam;
//rand_s関数のラッパー関数
unsigned int Random()
{
static char init;
if (init == 0) {
SYSTEMTIME systemTime;
FILETIME fileTime;
GetSystemTime(&systemTime);
SystemTimeToFileTime(&systemTime, &fileTime);
srand((unsigned)fileTime.dwLowDateTime);
init = 1;
}
unsigned int r;
return rand_s(&r) == 0 ? r : 0;
}
//スレッド実行関数
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
ThreadParam* tp = (ThreadParam*)lpParameter;
//初期化に時間がかかることを仮定
Sleep(Random() % 100 + 100);
//引数から必要なデータをコピーしておく
HWND hStatic = tp->hStatic;
int add = tp->add;
//初期化完了
//シグナル状態に設定する
SetEvent(tp->hEventThreadInitialized);
//ここまでの処理は別のスレッドと同時実行されることはない
//これ以降「tp->hStatic」と「tp->add」は
//参照先の値が書き換わる可能性があるので使用しない
int counter = 0;
WCHAR buf[BUFFERSIZE];
//シグナル状態になるまで待機
WaitForSingleObject(tp->hEventThreadAllInitialized, INFINITE);
while (1) {
//イベントを利用して一時停止する処理
WaitForSingleObject(tp->hEventPause, INFINITE);
StringCchPrintf(buf, BUFFERSIZE, L"%d", counter += add);
SetWindowText(hStatic, buf);
Sleep(10);
}
return 0;
}
//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static ThreadParam tp;
static BOOL bPause;
HWND hStatics[3];
int add[3];
HANDLE hThread;
switch (message)
{
case WM_CREATE: //ウィンドウの作成
//自動リセットイベント、初期値はノンシグナル状態
tp.hEventThreadInitialized = CreateEvent(NULL, FALSE, FALSE, NULL);
if (!tp.hEventThreadInitialized) {
return -1;
}
//手動リセットイベント、初期値はノンシグナル状態
tp.hEventThreadAllInitialized = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!tp.hEventThreadAllInitialized) {
return -1;
}
//手動リセットイベント、初期値はシグナル状態
tp.hEventPause = CreateEvent(NULL, TRUE, TRUE, NULL);
if (!tp.hEventPause) {
return -1;
}
CreateWindow(L"BUTTON", L"一時停止",
WS_CHILD | WS_VISIBLE,
10, 10,
120, 25,
hWnd, (HMENU)IDC_BUTTON1, hInst, NULL);
hStatics[0] = CreateWindow(L"STATIC", L"0",
WS_CHILD | WS_VISIBLE,
10, 40,
80, 25,
hWnd, (HMENU)-1, hInst, NULL);
hStatics[1] = CreateWindow(L"STATIC", L"0",
WS_CHILD | WS_VISIBLE,
100, 40,
80, 25,
hWnd, (HMENU)-1, hInst, NULL);
hStatics[2] = CreateWindow(L"STATIC", L"0",
WS_CHILD | WS_VISIBLE,
190, 40,
80, 25,
hWnd, (HMENU)-1, hInst, NULL);
add[0] = 1;
add[1] = 2;
add[2] = -1;
for (int i = 0; i < 3; i++) {
//スレッド毎に異なる値をセット
tp.hStatic = hStatics[i];
tp.add = add[i];
//スレッド作成
hThread = (HANDLE)_beginthreadex(
NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadProc,
&tp, 0, NULL);
if (hThread) {
CloseHandle(hThread);
//スレッドの初期化が終わるまで待機
WaitForSingleObject(tp.hEventThreadInitialized, INFINITE);
}
}
//全てのスレッドの作成と初期化が終了したことを
//各スレッドに通知
SetEvent(tp.hEventThreadAllInitialized);
break;
case WM_COMMAND: //コントロール操作
switch (LOWORD(wParam))
{
case IDC_BUTTON1:
bPause = !bPause;
if (bPause) {
ResetEvent(tp.hEventPause);
SetWindowText((HWND)lParam, L"一時停止解除");
//LPARAMはボタンのハンドル
}
else {
SetWindowText((HWND)lParam, L"一時停止");
SetEvent(tp.hEventPause);
}
break;
}
break;
case WM_DESTROY: //ウィンドウの破棄
if (tp.hEventThreadInitialized) CloseHandle(tp.hEventThreadInitialized);
if (tp.hEventThreadAllInitialized) CloseHandle(tp.hEventThreadAllInitialized);
if (tp.hEventPause) CloseHandle(tp.hEventPause);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
このコードは左からカウンタを「+1」「+2」「-1」で加算していきます。
スレッドの初期化時にSleep関数でランダムに待機させています。
通常であればスレッド毎の動作は独立しているので、「先に作成されたスレッドの初期化が先に終わる」という保証はありませんが、イベントを利用してひとつのスレッドの初期化が終わるまで次のスレッドの作成を待機し、スレッドの初期化中に他のスレッドの初期化処理が重複しないようにしています。
そのため、スレッド毎に引数に異なる値を設定して使いまわすことができます。
スレッドの初期化後は、他のすべてのスレッドの初期化が終わるまで待機させています。
これにより、全てのスレッドの実行タイミングをそろえています。
当然ですが、これらの処理中はシングルスレッドとほぼ同じことをしているのでマルチスレッドによる高速化の恩恵はありません。
なお、今回のコードはメインスレッド中でWaitForSingleObject
関数を実行してメインスレッドを待機させています。
本来はこういう事をするとウィンドウがフリーズしてしまいますが、今回はウィンドウ作成前の処理なので、起動は少し遅くなりますが見た目上の変化はありません。
ウィンドウの作成後にこのような処理をしたい場合は、各スレッドを待機する専用のスレッドをもうひとつ作るなどの工夫が必要です。