いまさらWindows API㉛- 同期・その2(TryEnterCriticalSection)

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

はじめに

先のクリティカルセクションを使ったプログラムarques.hatenablog.comでは、EnterCriticalSection API を使ったため処理がブロックされます(所有権を獲得するまで制御が戻ってこない)。せっかくスレッドを使ったのに、効率の悪いプログラムになってしまう可能性があります。このため、制御がブロックされないTryEnterCriticalSection 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;

CRITICAL_SECTION cSection;
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) {
        while (TryEnterCriticalSection(&cSection) == 0)
            std::cout << ".";

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

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

        LeaveCriticalSection(&cSection);
    }

    return 0;
}

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

    InitializeCriticalSection(&cSection);

    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);
        }
    }

    DeleteCriticalSection(&cSection);

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

先のクリティカルセクションを使ったプログラム(いまさらWindows API㉚- 同期・その1(クリティカルセクション) - arques’s diary)と異なるのは、ごく僅かです。
先のプログラムで

EnterCriticalSection(&cSection);

だった部分を

while (TryEnterCriticalSection(&cSection) == 0)
    std::cout << ".";

は書き換えるだけです。
EnterCriticalSection API は処理をブロック(所有権を獲得するまで制御が戻ってこない)します。これでは、せっかくスレッドを使ったのに効率の悪いプログラムになってしまう可能性があります。ここでは制御がブロックされないTryEnterCriticalSection API を使用します。本プログラムは、クリティカルセクションオブジェクトの所有権を獲得できるまで、cout へ"."(ピリオド)を表示します。これによって、スレッドが、どの程度ストールしているか分かります。

ビルド&実行

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

 ビルド&実行

C:\>cl /EHsc /DUNICODE /D_UNICODE ConsoleApplication1.cpp
...
C:\>.\ConsoleApplication1
.......................................................................
counter = 1...........................................
counter = 2.............................................
counter = 3...............................

 :

counter = 114..........................
counter = 115.............................counter = 116..........
counter = 117..............................
counter = 118..............................

 :

counter = 293
counter = 294
counter = 295
counter = 296
counter = 297
counter = 298
counter = 299
counter = 300
All threads have completed.


スレッドが多数並行動作しているため、沢山の"."(ピリオド)が表示されます。これは、スレッド間でオブジェクト取得の競合が起きていることを示します。このように、TryEnterCriticalSection API は処理をブロックしないため、ほかに処理しなければならない作業がある時に、効率的に使用できるAPI です。

API

TryEnterCriticalSection

クリティカルセクションオブジェクトの所有権獲得をブロックせずに試みます。

BOOL TryEnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection // クリティカルセクション
);
引数
lpCriticalSection クリティカルセクションオブジェクトへのポインタです。
戻り値

指定したクリティカルセクションに入ることができた場合、または現在のスレッドがクリティカルセクションをすでに所有している場合は、0 以外の値が返ります。指定したクリティカルセクションをすでに別のスレッドが所有しているときは、0 が返ります。