hozugawa.net · A neutral informational page

CreateEventとSetEventでイベント同期を行う

Windows APIで複数のスレッドを協調動作させるとき、イベントオブジェクトは「処理が完了した」「データを準備した」といった状態を通知するために使われます。CreateEventでイベントを作成し、待機側がWaitForSingleObjectなどで停止し、通知側がSetEventを呼び出すのが基本的な流れです。

イベントは共有変数を繰り返し確認するポーリングよりも効率的で、待機中のスレッドはCPU時間をほとんど消費しません。メッセージループやファイル処理など、Windowsプログラミングで非同期処理を組み合わせる場合にも役立ちます。ウィンドウメッセージの流れを整理するときは、ウィンドウプロシージャの仕組みも関連する知識になります。

項目 内容
作成 CreateEventでイベントカーネルオブジェクトを作成
待機 WaitForSingleObjectやWaitForMultipleObjectsを使用
通知 SetEventでシグナル状態に変更
リセット ResetEventまたは自動リセットで非シグナル状態へ戻す
解放 使用後にCloseHandleを呼び出す
主な用途 スレッド間通知、処理完了通知、ワーカースレッド制御

イベントオブジェクトの基本動作

CreateEventの宣言は、イベントのセキュリティ属性、手動リセットかどうか、初期状態、名前を指定する形です。C言語では次のように作成できます。

HANDLE hEvent = CreateEvent(
    NULL,   // セキュリティ属性
    FALSE,  // FALSE: 自動リセット
    FALSE,  // 初期状態は非シグナル
    NULL    // 名前なし
);

第2引数がFALSEなら自動リセットイベント、第2引数がTRUEなら手動リセットイベントです。第3引数をTRUEにすると、作成直後からシグナル状態になります。作成に失敗した場合はNULLが返るため、必ず戻り値を確認します。

待機側は、イベントがシグナル状態になるまで処理を停止します。自動リセットイベントでは、待機中のスレッドが1つ解放されると、イベントは自動的に非シグナル状態へ戻ります。処理完了を一度だけ通知する用途や、単一のワーカースレッドを起こす用途に向いています。

SetEventとResetEventの使い分け

通知側のスレッドは、必要なデータの準備が終わった時点でSetEventを実行します。イベントを待っていたスレッドが再開した後、共有データを読み取るという構成です。

// 通知側
PrepareData();
SetEvent(hEvent);

// 待機側
DWORD result = WaitForSingleObject(hEvent, INFINITE);
if (result == WAIT_OBJECT_0) {
    ConsumeData();
}

手動リセットイベントでは、SetEventを呼ぶとイベントがシグナル状態のまま維持されます。そのため、複数の待機スレッドをまとめて解放できます。通知を受けた全スレッドが処理を終えた後など、ResetEventを呼んで明示的に状態を戻す設計が必要です。

SetEventを呼ぶタイミングが早すぎると、待機側がデータを読む前に準備処理が完了していない状態になる可能性があります。イベントはデータそのものを保護する機能ではないため、共有データへの同時アクセスが発生する場合は、クリティカルセクションやミューテックスなども併用します。

自動リセットと手動リセットの選択

自動リセットイベントは、通知を1回消費するキューのような動作をします。ただし、イベントは通知回数を蓄積しません。SetEventを複数回呼んでも、待機スレッドが受け取る前に状態が変わらなければ、複数回分の通知として扱われない点に注意が必要です。

手動リセットイベントは、複数スレッドへ一斉に開始を知らせる用途に適しています。たとえば、初期化が終わったことを全ワーカーへ伝える場合、全スレッドが同じイベントを待機し、初期化側がSetEventを1回呼ぶだけで済みます。

スレッドを順番に1つずつ動かすなら自動リセット、全員に同じ状態変化を知らせるなら手動リセットという基準が使えます。イベントの意味を「一回の合図」とするのか「状態の公開」とするのかを先に決めると、ResetEventの扱いも明確になります。

待機処理とエラー確認

WaitForSingleObjectの戻り値は、WAIT_OBJECT_0、WAIT_TIMEOUT、WAIT_ABANDONED、WAIT_FAILEDなどに分かれます。無限待機だけでなくタイムアウトを設定すれば、異常停止したスレッドや外部処理にも対応できます。

DWORD result = WaitForSingleObject(hEvent, 5000);

if (result == WAIT_OBJECT_0) {
    // 通知を受信
} else if (result == WAIT_TIMEOUT) {
    // 5秒以内に通知されなかった
} else if (result == WAIT_FAILED) {
    // GetLastErrorで原因を確認
}

名前付きイベントを使うと、別プロセス間でも同じカーネルオブジェクトを共有できます。CreateEventで同名のイベントがすでに存在する場合、戻り値は取得できますが、GetLastErrorがERROR_ALREADY_EXISTSになることがあります。新規作成か既存オブジェクトの取得かを区別する設計では、この値を確認します。

Windows APIを調べる際は、実装例や関連機能をまとめたWindows APIの技術情報も参照先になります。イベント名の衝突、アクセス権、プロセス終了時のハンドル状態など、単純なスレッド間通信以外の条件も確認できます。

安全な実装に向けた確認事項

イベント同期は扱いやすい一方、待機と通知の順番を誤るとデッドロックや通知の取りこぼしが起きます。特に、通知側がSetEventを実行した後に待機側がイベントを作り直す構成や、手動リセットイベントを戻し忘れる構成は避けなければなりません。

ハンドルは所有者を明確にし、すべての終了経路でCloseHandleを呼び出します。スレッドが終了した後にイベントを破棄するなど、利用中のハンドルを先に閉じると未定義の動作につながります。ファイル選択ダイアログなど別のモーダル処理を組み込む場合も、関連するファイル選択ダイアログがスレッドの待機を妨げないか確認します。

実装前には、イベントの種類、通知対象、リセットの担当、終了時の扱いを決めておくと安全です。

イベント同期の要点は、CreateEventで表現した状態を待機側と通知側で共有し、SetEventを実行する責任を一箇所に定めることです。まずは自動リセットイベントと有限タイムアウトを使った小さな構成で動作を確認し、必要に応じて手動リセットや複数オブジェクト待機へ拡張すると、安定したWindows APIプログラムに仕上げやすくなります。