イベント

排他制御や同期制御の方法としてクリティカルセクションミューテックスセマフォなどを説明しました。
これらはオブジェクトの所有権やリソース数などで状態(シグナル状態/ノンシグナル状態)を変化させますが、もっとシンプルなものにイベントオブジェクトがあります。
イベントオブジェクトの状態は任意のスレッド(プロセス)から任意のタイミングで、特別な条件なしに設定できます。

イベント関数

イベント関数の使い方はミューテックス関数やセマフォ関数と基本的に同じです。
スレッドの待機には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というアクセス権を要求します。
この場合、引数bManualResetbInitialStateはすでに設定済みであるため無視されます。
引数lpEventAttributesbInheritHandleメンバにより、ハンドルの子プロセスへの継承を設定することは可能ですが、セキュリティ記述子は無視されます。

名前は大文字小文字を区別します。
長さは最大で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関数を実行してメインスレッドを待機させています。
本来はこういう事をするとウィンドウがフリーズしてしまいますが、今回はウィンドウ作成前の処理なので、起動は少し遅くなりますが見た目上の変化はありません。
ウィンドウの作成後にこのような処理をしたい場合は、各スレッドを待機する専用のスレッドをもうひとつ作るなどの工夫が必要です。