いまさらWindows API㉟- 同期・その6(イベント)

この時代にWindows APIのプログラムの解説です。

はじめに

スレッドをたくさん生成し、それらをイベントで同期させる方法について解説します。ほとんどセマフォの例と同じなので、簡単に説明します。

プロジェクト

Visual Studio を起動し、コンソールアプリを選択してください。生成された*.cppファイルを上記リストで書き換えます。

C++コード

ミューテックスを使ったプログラムと、本プログラムの違いを示します。

ソースコード全体も示します。

#include <windows.h>
#include <iostream>
#include <array>
#include <thread>   // for std::this_thread::sleep_for
#include <chrono>   // for std::chrono::milliseconds

constexpr int NUM_OF_THREADS = 6;
constexpr int COUNTER_LIMIT = 50;

HANDLE event = nullptr;
volatile unsigned int counter = 0;

// スレッド関数
static DWORD WINAPI countThread(LPVOID param) {
    int counterLimit = static_cast<int>(reinterpret_cast<INT_PTR>(param));

    for (int i = 0; i < counterLimit; ++i) {
        WaitForSingleObject(event, INFINITE);   // イベント待機

        unsigned int x = counter;
        ++x;
        std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 遅延
        counter = x;

        std::cout << "counter = " << counter << std::endl;

        SetEvent(event);                        // イベントシグナル
    }

    return 0;
}

int main() {
    std::array<HANDLE, NUM_OF_THREADS> threads{};

    // 手動リセット: FALSE、自動リセット(シングルスレッド通過)
    event = CreateEvent(nullptr, FALSE, TRUE, nullptr);
    if (!event) {
        std::cerr << "Failed to create event." << std::endl;
        return 1;
    }

    for (int i = 0; i < NUM_OF_THREADS; ++i) {
        threads[i] = CreateThread(
            nullptr, 0,
            countThread,
            reinterpret_cast<LPVOID>(static_cast<INT_PTR>(COUNTER_LIMIT)),
            0, nullptr
        );
    }

    // 全スレッドの終了を待機
    WaitForMultipleObjects(NUM_OF_THREADS, threads.data(), TRUE, INFINITE);

    // ハンドルを閉じる
    for (HANDLE h : threads) {
        if (h) {
            CloseHandle(h);
        }
    }

    if (event) {
        CloseHandle(event);
    }

    std::cout << "All threads have completed." << std::endl;
    return 0;
}

まず、CreateEvent API を使ってイベントオブジェクトを作成します。ほとんどセマフォオブジェクトのプログラムと同様で、スレッドの生成も同じです。
イベントオブジェクトは、手動リセットオブジェクトを作成するか、自動リセットオブジェクトを作成するかを指定できます。この例は自動リセットオブジェクトを使用します。自動リセットオブジェクトは、待機スレッドがなくなった時点で、非シグナル状態となります。
手動リセットオブジェクトで生成した場合、イベントオブジェクトのシグナル状態は、自分で管理しなければなりません。
セマフォオブジェクトの例と同じようなプログラムです。スレッドの生成も同じです。イベントオブジェクトを非シグナル状態にするにはWaitForSingleObject API を、シグナル状態にするにはSetEvent API 使用します。ほかは、これまでと同様です。

ビルド&実行

Visual Studio からビルド&実行する場合は、F5を押してください。
あるいはVisual Studio の開発コンソールを利用します。

 ビルド&実行

C:\>cl /EHsc /DUNICODE /D_UNICODE ConsoleApplication1.cpp
...
C:\>.\ConsoleApplication1
counter = 1
counter = 2
counter = 3
counter = 4
counter = 5
counter = 6
counter = 7
:
counter = 290
counter = 291
counter = 292
counter = 293
counter = 294
counter = 295
counter = 296
counter = 297
counter = 298
counter = 299
counter = 300
All threads have completed.

API

CreateEvent

名前付き、または名前なしのイベントオブジェクトを作成、または開きます。

HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // セキュリティ記述子
BOOL bManualReset, // リセットのタイプ
BOOL bInitialState, // 初期状態
LPCTSTR lpName // イベントオブジェクトの名前
);
引数
lpEventAttributes 子プロセスが、取得したハンドルを継承できるかどうかを決定するSECURITY_ATTRIBUTES 構造体へのポインタです。NULL を指定すると、既定のセキュリティ記述子が割り当てられます。
bManualReset TRUE を指定すると、手動リセットオブジェクトが、FALSE を指定すると、自動リセットオブジェクトが作成されます。
bInitialState イベントオブジェクトの初期状態を指定します。TRUE を指定すると、シグナル状態に設定されます。FALSE を指定すると、非シグナル状態に設定されます。
lpName イベントオブジェクトの名前を保持している文字列へのポインタです。NULL を指定すると、名前なしのイベントオブジェクトが作成されます。
戻り値

成功すると、イベントオブジェクトのハンドルが返ります。指定した名前付きイベントオブジェクトがすでに存在した場合、そのオブジェクトのハンドルが返ります。関数が失敗すると、NULL が返ります。

SetEvent

指定されたオブジェクトをシグナル状態に設定します。

BOOL SetEvent(
HANDLE hEvent // イベントオブジェクトのハンドル
);
引数
hEvent イベントオブジェクトのハンドルです。
戻り値

成功すると、0 以外の値が返ります。失敗すると、0 が返ります。