Windows の C で Guarded Suspension パターンを実現する (条件変数を実装する)

マルチスレッドプログラミングをしているときに、「ある条件が満たされているかどうか調べ、満たされていなければ条件が満たされるまで待機する」 という処理が必要になることがあります。 このような処理はパターン化されており、『増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編』 では Guarded Suspension パターンと呼ばれています。

POSIX のマルチスレッドが使える環境では、条件変数 (condition variable) pthread_cond を使って Guarded Suspension パターンを実現することができます。 しかし、Windows のスレッド機能には条件変数に相当するものはありません。

本記事では、Windows が提供している CriticalSection と Event を使って条件変数を実装し、Windows の C で Guarded Suspension パターンを実現する方法を説明します。

基本的な考え方

条件変数を使って Guarded Suspension パターンを実現する際の基本的な流れを説明します。

  • ミューテックスのロックを取得する
  • 条件が満たされているかどうか調べる (*)
    • 条件が満たされていれば次の処理に移る
    • 条件が満たされていなければ 条件変数の上で待つ → 他のスレッドから信号が来たらスレッド再開し (*) の処理に戻る
  • 何らかの処理をする
  • ミューテックスのロックを解放する

条件というのは、何らかの処理をするために満たされている必要がある条件です。 例えば、「変数 count が 0 以上である」 というような条件です。 当然ながら、条件に含まれる変数 (この例では count) は複数のスレッドから読み書きされるものなので、ミューテックスによる排他制御をしなければいけません。 上の説明で出てくるミューテックスは、条件に含まれる変数を保護するためのものです。

なお、条件変数の上で待つ際に、ミューテックスのロックを解放しなければ他のスレッドが条件に含まれる変数の値を変更できなくなってしまいデッドロックしてしまいます。 そうならないように、条件変数の上で待つときに自動的にミューテックスのロックを解放し、再開するときにミューテックスのロックを取得するようになっています。

条件変数の実装

さて、それでは Windows で条件変数を実装する手法を説明します。 条件変数を実装する上で重要なのは次の処理です。

  • 他のスレッドからシグナルが送られてくるのを待つ処理
  • 他のスレッドからシグナルが送られてくるのを待つときに、ミューテックスのロックを解放し、スレッドを再開するときにロックを取得する処理
  • 条件に含まれる変数を書き換えたときにシグナルを送信する処理

Windows ではシグナルのやりとりは Event を使って行います。 ミューテックスは CriticalSection を使用します *1。 また、pthread_cond では、条件変数の上で待機しているスレッドの中から 1 つだけ再開させるのか、待機しているスレッドを全て再開させるのか選ぶことができます。 今回はこの処理も同じように実装します。

まず条件変数を表す構造体ですが、シグナルのやり取りのための Event だけを扱えればいいので次のようになります。 Event が 2 つありますが、これは待機しているスレッドの中から 1 つだけ再開させるためのシグナルと、全てのスレッドを再開させるシグナルに対応しています。

typedef struct {
    HANDLE hEventsWakeup[2];
} COND_VARIABLE;

構造体 COND_VARIABLE の初期化処理と終了処理のための関数は次のようになります。

// 初期化処理を行う関数
void CondVariable_init( COND_VARIABLE *self ) {
    // 1 つのスレッドを起こすためのイベント
    self->hEventsWakeup[0] = CreateEvent( NULL, FALSE, FALSE, NULL );
    // 全てのスレッドを起こすためのイベント
    self->hEventsWakeup[1] = CreateEvent( NULL, TRUE,  FALSE, NULL );
}
// 終了処理を行う関数
void CondVariable_final( COND_VARIABLE *self ) {
    CloseHandle( self->hEventsWakeup[0] );
    CloseHandle( self->hEventsWakeup[1] );
}

また、条件が満たされていない場合に待機するための関数は次のようになります。 第 2 引数はロック済みのミューテックスをあらわします。 第 3 引数は最大の待ち時間です。 他のスレッドが起こすまでいつまでも待ち続けるならば INFINITE を渡します。

// 条件変数の上で待つための関数
void CondVariable_wait( COND_VARIABLE *self, CRITICAL_SECTION *cs_ptr, DWORD dwMilliseconds ) {
    ResetEvent( self->hEventsWakeup[0] );
    ResetEvent( self->hEventsWakeup[1] );
    LeaveCriticalSection( cs_ptr );
    WaitForMultipleObjects( 2, self->hEventsWakeup, FALSE, dwMilliseconds );
    EnterCriticalSection( cs_ptr );
}

条件に含まれる変数の値を変更した際に待機しているスレッドに通知を行うための関数は次のようになります。

// 待機しているスレッドのうち 1 つのスレッドを起こす関数
void CondVariable_notify( COND_VARIABLE *self ) {
    SetEvent( self->hEventsWakeup[0] );
}
// 待機しているスレッド全てを起こす関数
void CondVariable_notifyAll( COND_VARIABLE *self ) {
    SetEvent( self->hEventsWakeup[1] );
}

使用例

「変数 shCount が 0 より大きい」 というのを条件にして、条件が満たされていれば shCount をデクリメントして "process..." という文字列を出力するスレッドを 5 個動かすプログラムです。 メインスレッドでは約 1 秒ごとに shCount をインクリメントするので、約 1 秒ごとに "process..." という文字列が出力されます。

#include <stdio.h>
#include <process.h>
#include <windows.h>

// VARIABLE_COND の構造体と関数
typedef struct {
    HANDLE hEventsWakeup[2];
} COND_VARIABLE;
void CondVariable_init( COND_VARIABLE *self ) {
    // notify のためのイベント
    self->hEventsWakeup[0] = CreateEvent( NULL, FALSE, FALSE, NULL );
    // notifyAll のためのイベント
    self->hEventsWakeup[1] = CreateEvent( NULL, TRUE,  FALSE, NULL );
}
void CondVariable_final( COND_VARIABLE *self ) {
    CloseHandle( self->hEventsWakeup[0] );
    CloseHandle( self->hEventsWakeup[1] );
}
void CondVariable_wait( COND_VARIABLE *self, CRITICAL_SECTION *cs_ptr, DWORD dwMilliseconds ) {
    ResetEvent( self->hEventsWakeup[0] );
    ResetEvent( self->hEventsWakeup[1] );
    LeaveCriticalSection( cs_ptr );
    WaitForMultipleObjects( 2, self->hEventsWakeup, FALSE, dwMilliseconds );
    EnterCriticalSection( cs_ptr );
}
void CondVariable_notify( COND_VARIABLE *self ) {
    SetEvent( self->hEventsWakeup[0] );
}
void CondVariable_notifyAll( COND_VARIABLE *self ) {
    SetEvent( self->hEventsWakeup[1] );
}

// スレッド間で共有されるデータ
int shCount = 0;
// スレッド間で共有されるデータを保護するための排他処理, 同期機構
CRITICAL_SECTION shCS;
COND_VARIABLE    shCV;

// 子スレッドで行う処理
static unsigned __stdcall run( void *arg ) {
    // ミューテックスのロックを取得
    EnterCriticalSection( &shCS );
    // 条件が満たされているかどうかチェック
    while( shCount <= 0 ) {
        printf( "wait...\n" );
        fflush( stdout );
        // 条件が満たされていないので待機する
        CondVariable_wait( &shCV, &shCS, INFINITE );
        printf( "wakeup!\n" );
    }
    shCount--;
    printf( "decrease shCount: %d\n", shCount );
    printf( "process...\n" );
    fflush( stdout );
    // ミューテックスのロックを解放
    LeaveCriticalSection( &shCS );
    _endthreadex(0);
    return 0; // コンパイラの警告を殺す
}

int main( int argc, char **argv ) {
    int numSign = 5;
    HANDLE hThreads[numSign];
    InitializeCriticalSection( &shCS );
    CondVariable_init( &shCV );
    for( int i = 0; i < numSign; i++ ) {
        hThreads[i] = (HANDLE)_beginthreadex( NULL, 0, run, NULL, 0, NULL );
    }
    // スレッドの数だけカウンタをインクリメントする
    for( int i = 0; i < numSign; i++ ) {
        Sleep( 1000 );
        // 共有変数をいじるのでミューテックスのロックを取得
        EnterCriticalSection( &shCS );
        shCount++;
        printf( "increase shCount: %d\n", shCount );
        fflush( stdout );
        // 条件に含まれる変数を変更したので待機しているスレッドを起こす
        CondVariable_notify( &shCV );
        // ミューテックスのロックを解放
        LeaveCriticalSection( &shCS );
    }
    // 全てのスレッドが終了するまで待つ
    WaitForMultipleObjects( numSign, hThreads, TRUE, INFINITE );
    DeleteCriticalSection( &shCS );
    CondVariable_final( &shCV );
    return 0;
}

エラー処理など

この記事のコードは勉強用に書いたものなので、エラー処理等はちゃんとしていません。 実際に Windows で条件変数を使う場合はもうちょっとエラー処理等をちゃんとしないとだめだと思います。

*1:Windows には CriticalSection のほかに Mutex がありますが、プロセス間の排他処理をしないのであれば CriticalSection を使ったほうが高速なので良いようです。

Windows 上に C/C++ 開発環境を構築する #1 (MinGW のインストール方法)

Windows 上で C/C++ の開発を行う際にどの C/C++ コンパイラを使用するかというのは 1 つの悩みどころです。 Windows 専用のアプリケーションの開発を行うのであれば Microsoft Visual C++ でいいと思いますが、できるだけ Unix/Linux 環境に近い状態にしたいという人も居ると思います。

そういう人の 1 つの選択肢として GNU Compiler Collection (GCC)Windows 移植版である MinGW が挙げられます。 MinGW を使用すると、Windows 上で C や C++Objective-C などのコンパイルが可能となります。 この記事では、MinGWWindows にインストールする方法を記します。 (MinGW 本家のインストールガイド に全部書いてますけども。)

GUIインストーラを使ってインストール

下記ページから、MinGW をインストールするための GUI インストーラをダウンロードします。 "Looking for the latest version? Download mingw-get-inst-XXXXXXXX.exe (XXX.X KB)" と書いてある場所からダウンロードしてください。

ダウンロードしたファイルを実行します。 インストーラに含まれているファイルをインストールに使うか、最新版をインターネット上からダウンロードしてインストールするか、といったことや、インストールするコンポーネント (C コンパイラC++ コンパイラ、など) の選択を行います *1。 また、インストール先フォルダの選択も行います。 各自好きなものを選んでください。 コンポーネントに関して言えば、とりあえずは最低限必要なものを選んでおけばよいでしょう。 インストール先は特に変える理由がなければ "C:\MinGW" が良いとおもいます。

適当に選択してインストールを開始すると、後は放っておけばインストールしてくれます。

パスを通す作業

最後に、コマンドプロンプトgcc などのコマンドをたたいたときに、今インストールした gcc などが起動されるようにパスを通す作業を行います。

コントロールパネルなどから "システムのプロパティ" ウィンドウを開きます。 このウィンドウの "詳細設定" タブを選択し、"環境変数" ボタンをクリックします。 "システム環境変数" 一覧の中から "Path" という変数を選び、最後尾 *2 に ";C:\MinGW\bin" *3 を追加します。 これでコマンドプロンプトから gcc などのコマンドが使用できるようになるはずです。

これでインストール作業は終了です。

make コマンドについて

最後に make コマンドについてちょっと書いておきます *4MinGW で提供されている make コマンドには "mingw32-make.exe" という名前が付いており、コマンドプロンプトから使用する場合は "mingw32-make" とコマンドを打たなければいけません。 なぜこのような名前が付いているのかは以下に書いてあります。

つまり、MSYS にも make が含まれており、名前の衝突を防ぐために MinGW の make の名前を変更した、ということのようです。 MSYS をインストールして make を使うのであればこのままでいいと思いますが、MSYS をインストールしないのであれば mingw32-make.exe をコピーして make.exe にリネームしてもいいかと思います。

参考文献

MinGW のインストールにあたっては以下を参考にしました。

*1:1 つのコンポーネントには複数のコマンドが含まれます。 例えば C コンパイラコンポーネントには、gcc コマンドはもちろん、gdb や make など、数多くのコマンドが含まれます。

*2:別に最後尾じゃなくてもいいですが...

*3:"C:\MinGW" 以外の場所にインストールした人は、インストールした場所を設定してください。

*4:C コンパイラのインストールを行うと、make コマンドも自動的にインストールされます。

C において配列へのポインタと配列長を 1 つの構造体にまとめる

C では (malloc 関数などによりメモリ領域を確保した場合の) 配列の長さを知る事は難しいと思います。 そのため、関数の引数として配列を渡す場合などに、初歩的な教科書ではよく配列へのポインタと同時に配列の長さを渡していると思います。

/* 配列長 */
size_t length = 5;
/* 配列へのポインタ */
int* arrayPt = (int*) malloc( sizeof(int) * length );

/* 別の関数に配列へのポインタを渡す際に, 同時に配列長も渡す事が多い */
function( arrayPt, length );

まあこの方法でもいいですけど、やはり 1 つにまとめた方がすっきりしますよね。 というわけで配列へのポインタと配列長を 1 つの構造体にまとめてしまいましょう。

3 次元配列の場合のサンプル

3 次元配列の場合のサンプルを以下に書いておきます。

#include <stdio.h>
#include <malloc.h>

/**
* 3 次元配列の実体と長さを 1 つにまとめた構造体
*/
typedef struct {
size_t length1; /* 1 軸方向の要素数 */
size_t length2; /* 2 軸方向の要素数 */
size_t length3; /* 3 軸方向の要素数 */
int*** e;
int** _e1;
int* _e2; /* 3 次元配列の実体 */
} ArrayInt3;
typedef ArrayInt3 *ArrayInt3Pt;

/**
* 構造体 ArrayInt3 用にメモリ確保を行う関数
* この関数を利用してメモリ確保した場合, プログラム終了前に delete_ArrayInt3 関数で
* メモリ領域の解放を行わなければいけない.
* @param size1 3 次元配列の 1 軸方向の要素数
* @param size2 3 次元配列の 2 軸方向の要素数
* @param size3 3 次元配列の 3 軸方向の要素数
* @return 確保したメモリ領域へのポインタ
*/
ArrayInt3Pt new_ArrayInt3( size_t size1, size_t size2, size_t size3 ) {
size_t i;
ArrayInt3Pt pt = (ArrayInt3Pt) malloc( sizeof(ArrayInt3) );
if ( (pt->_e2 = (int*) malloc( sizeof(int) * size1 * size2 * size3 )) == NULL ) {
return NULL;
}
if ( (pt->_e1 = (int**) malloc( sizeof(int*) * size1 * size2 )) == NULL ) {
free( pt->_e2 );
return NULL;
}
if ( (pt->e = (int***) malloc( sizeof(int**) * size1 )) == NULL ) {
free( pt->_e2 );
free( pt->_e1 );
return NULL;
}
for ( i = 0; i < size1; i++ ) {
pt->e[i] = pt->_e1 + i * size2;
}
for ( i = 0; i < size1 * size2; i++ ) {
pt->_e1[i] = pt->_e2 + i * size3;
}
pt->length1 = size1;
pt->length2 = size2;
pt->length3 = size3;
return pt;
}

/**
* 構造体 ArrayInt3 用確保したメモリ領域の解放を行う関数
* @param pt ArrayInt3 構造体用に確保したメモリ領域へのポインタ
* @return なし
*/
void delete_ArrayInt3( ArrayInt3Pt pt ) {
if ( pt != NULL ) {
if ( pt->_e2 != NULL ) { free( pt->_e2 ); pt->_e2 = NULL; }
if ( pt->_e1 != NULL ) { free( pt->_e1 ); pt->_e1 = NULL; }
if ( pt->e != NULL ) { free( pt->e ); pt->e = NULL; }
free( pt );
}
}

int main(void) {

/* 変数宣言 */
size_t i, j, k;
ArrayInt3Pt arrayPt = NULL;

/* 4x10x3 の 3 次元配列の生成 */
arrayPt = new_ArrayInt3(4,10,3);
if ( arrayPt == NULL ) { return -1; }

/* 多重 for 文にて配列にアクセスする */
for ( i = 0; i < arrayPt->length1; i++ ) {
for ( j = 0; j < arrayPt->length2; j++ ) {
for ( k = 0; k < arrayPt->length3; k++ ) {
arrayPt->e[i][j][k] = i * j * k;
printf( "%d ", arrayPt->e[i][j][k] );
}
puts("");
}
puts("");
}

/* メモリ領域解放 */
delete_ArrayInt3(arrayPt);
arrayPt = NULL;

/* 終了 */
return 0;

}

「配列へのポインタ」 という言葉について

一般的にどういう言い方をするのかよくわからなかったので 「配列へのポインタ」 と言っていますが、単に int 型用のメモリ領域の並びを配列だと思っているだけで、「配列」 という呼び方は正しくないかもしれません。 (上の例では) 実際にはただの int 型用のメモリ領域へのポインタです。