マルチスレッドのプログラミング

第 4 章 同期オブジェクトを使ったプログラミング

この章では、スレッドで使用できる同期の手法と同期上の問題について説明します。

同期オブジェクトは、データと同じようにしてアクセスされるメモリー内の変数です。異なるプロセス内のスレッドは、通常はお互いに参照できませんが、スレッドが制御する共有メモリー内に格納されている同期オブジェクトを使用することにより、相互に同期をとることができます。

同期オブジェクトをファイルに置くこともできます。そうすれば、同期オブジェクトを作成したプロセスの消滅後も同期変数を有効にできます。

次の同期オブジェクトがあります。

次に、マルチスレッド処理での同期の必要性を説明します。


注 -

32 ビットアーキテクチャでは、long long 型は原子的 [「原子的」操作とは、それ以上細かく分割できない操作を意味します。] な処理対象ではなく、2 つの 32 ビット値として読み書きされます。int 型、char 型、float 型、およびポインタは、SPARC マシンと x86 マシンでは原子的です。


相互排他ロック属性

相互排他ロック (mutex ロック) は、スレッドの実行を直列化したいときに使用します。相互排他ロックでスレッド間の同期をとるときは、通常はコードの危険領域が複数のスレッドによって同時に実行されないようにするという方法が用いられます。単一のスレッドのコードを保護する目的で相互排他ロックを使用することもできます。

デフォルトの mutex 属性を変更するには、属性オブジェクトを宣言して初期化します。mutex 属性は、すばやく見つけて簡単に変更できるよう、通常はアプリケーションの先頭部分の一箇所で設定します。次の表に、この節で説明する mutex 属性操作関数を示します。

表 4-1 mutex 属性ルーチン

「mutex 属性オブジェクトの初期化」

「pthread_mutexattr_init(3T)」

「mutex 属性オブジェクトの削除」

「pthread_mutexattr_destroy(3T)」

「mutex のスコープ設定 」

「pthread_mutexattr_setpshared(3T)」

「mutex のスコープの値の取得 」

「pthread_mutexattr_getpshared(3T)」

mutex のスコープ定義について、Solaris と POSIX との相違点を表 4-2 に示します。

表 4-2 mutex の比較

Solaris 

POSIX 

定義 

USYNC_PROCESS

PTHREAD_PROCESS_SHARED

このプロセスと他のプロセスのスレッドの間で同期をとるために使用する 

USYNC_THREAD

PTHREAD_PROCESS_PRIVATE

このプロセスのスレッドの間でだけ同期をとるために使用する 

mutex 属性オブジェクトの初期化

pthread_mutexattr_init(3T)

pthread_mutexattr_init() は、このオブジェクトに関連付けられた属性をデフォルト値に初期化します。各属性オブジェクトのための記憶領域は、実行時にスレッドによって割り当てられます。

この関数が呼び出されたときの pshared 属性のデフォルト値は PTHREAD_PROCESS_PRIVATE で、初期化された mutex を 1 つのプロセスの中だけで使用できるという意味です。

プロトタイプ:
int	pthread_mutexattr_init(pthread_mutexattr_t *mattr);
#include <pthread.h>


pthread_mutexattr_t mattr;
int ret;

/* 属性をデフォルト値に初期化する */
ret = pthread_mutexattr_init(&mattr); 

mattr は不透明な型で、システムによって割り当てられた属性オブジェクトを指定します。mattr のスコープとして取りうる値は、PTHREAD_PROCESS_PRIVATE (デフォルト) と PTHREAD_PROCESS_SHARED です。

mutex 属性オブジェクトを再使用するには、pthread_mutexattr_destroy(3T) によって事前に削除しなければなりません。pthread_mutexattr_init() 呼び出しは、不透明なオブジェクト型へのポインタを戻します。そのオブジェクトが削除されないと、結果的にメモリーリークを引き起こします。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。


ENOMEM

メモリーが足りなくて、スレッド属性オブジェクトを初期化できません。


EINVAL

mattr で指定された値が無効です。

mutex 属性オブジェクトの削除

pthread_mutexattr_destroy(3T)

pthread_mutexattr_destroy() は、pthread_mutexattr_init() によって生成された属性オブジェクトの管理に使用されていた記憶領域の割り当てを解除します。

プロトタイプ:
int	pthread_mutexattr_destroy(pthread_mutexattr_t *mattr)
#include <pthread.h>

pthread_mutexattr_t mattr;
int ret;

/* 属性を削除する */
ret = pthread_mutexattr_destroy(&mattr); 

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

mattr で指定された値が無効です。

mutex のスコープ設定

pthread_mutexattr_setpshared(3T)

mutex 変数の値は、プロセス専用 (プロセス内) とシステム共通 (プロセス間) のどちらかです。pshared 属性を PTHREAD_PROCESS_SHARED 状態に設定して mutex を生成し、その mutex が共有メモリー内に存在する場合、その mutex は複数のプロセスのスレッドの間で共有できます。これは、オリジナルの Solaris スレッドにおいて mutex_init()USYNC_PROCESS フラグを使用するのに相当します。

プロトタイプ:
int	pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr,
    int pshared);
#include <pthread.h>

pthread_mutexattr_t mattr;
int ret;

ret = pthread_mutexattr_init(&mattr);
/*
 * デフォルト値にリセットする: private
 */
ret = pthread_mutexattr_setpshared(&mattr,
     PTHREAD_PROCESS_PRIVATE);

mutex の pshared 属性を PTHREAD_PROCESS_PRIVATE に設定した場合、その mutex を操作できるのは同じプロセスで生成されたスレッドだけです。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

mattr で指定された値が無効です。

mutex のスコープの値の取得

pthread_mutexattr_getpshared(3T)

プロトタイプ:
int	pthread_mutexattr_getpshared(pthread_mutexattr_t *mattr,
    int *pshared);
#include <pthread.h>

pthread_mutexattr_t mattr;
int pshared, ret;

/* mutex の pshared を取得する */
ret = pthread_mutexattr_getpshared(&mattr, &pshared); 

属性オブジェクト mattrpshared の現在値を取得します。これは PTHREAD_PROCESS_SHAREDPTHREAD_PROCESS_PRIVATE のどちらかです。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

mattr で指定された値が無効です。

相互排他ロックの使用方法

mutex の属性を設定後、mutex そのものを初期化します。以下の関数は、mutex の初期化、削除、ロック、ロック解除、およびブロックしないで行う mutex のロックに使用します。表 4-3 に、この章で説明する mutex ロック操作関数を示します。

表 4-3 相互排他ロック操作ルーチン

「mutex の初期化」

「pthread_mutex_init(3T)」

「mutex のロック」

「pthread_mutex_lock(3T)」

「mutex のロック解除」

「pthread_mutex_unlock(3T)」

「ブロックしないで行う mutex のロック」

「pthread_mutex_trylock(3T)」

「mutex の削除」

「pthread_mutex_destroy(3T)」

デフォルトスケジューリング方針 SCHED_OTHER は、スレッドによるロックの獲得順序を指定していません。複数のスレッドが mutex を待っているときの獲得の順序は不定です。衝突するときは、スレッドを優先順位でブロック解除するというのがデフォルト動作です。

mutex の初期化

pthread_mutex_init(3T)

pthread_mutex_init() は、mp が指す mutex をデフォルト値に初期化 (mattr が NULL の場合) するか、pthread_mutexattr_init() ですでに設定されている mutex 属性を指定するときに使用します。

プロトタイプ:
int	pthread_mutex_init(pthread_mutex_t *mp,
    const pthread_mutexattr_t *mattr);
#include <pthread.h>

pthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER;
pthread_mutexattr_t mattr;
int ret;

/* mutex をデフォルト値に初期化する */
ret = pthread_mutex_init(&mp, NULL);

/* mutex を初期化する */
ret = pthread_mutex_init(&mp, &mattr); 

初期化された mutex は、ロック解除状態になります。

mattr を NULL にするのは、デフォルト mutex 属性オブジェクトのアドレスを渡すのと同じことですが、メモリーのオーバーヘッドがありません。

静的に定義された mutex は、マクロ PTHREAD_MUTEX_INITIALIZER により、デフォルト属性をもつように直接初期化できます。

mutex ロックは、他のスレッドが使用している可能性がある間は再初期化したり削除したりしてはいけません。どちらの動作も正しく行われなければプログラムで障害が発生します。mutex を再初期化または削除する場合、アプリケーションはその mutex が現在使用されていないことを確認しなければなりません。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を戻します。


EBUSY

その mutex はまだ存在しているので、再初期化も削除もできません。


EINVAL

属性値が無効です。その mutex は変更されていません。


EFAULT

mp が指す mutex のアドレスが無効です。

mutex のロック

pthread_mutex_lock(3T)

プロトタイプ:
int	pthread_mutex_lock(pthread_mutex_t *mp); 
#include <pthread.h>

pthread_mutex_t mp;
int ret;

ret = pthread_ mutex_lock(&mp); /* mutex を獲得する */

pthread_mutex_lock() は、mp が指す mutex をロックします。mutex がすでにロックされている場合は、その mutex が使用可能になるまで呼び出しスレッドがブロックされ、優先順位別の待ち行列に入れられます。pthread_mutex_lock() が戻ると、呼び出しスレッドが mutex をロックした状態になっています。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

mp で指定された値が、初期化された mutex オブジェクトを表していません。


EDEADLK

現在のスレッドがすでにその mutex を獲得しています。

mutex のロック解除

pthread_mutex_unlock(3T)

pthread_mutex_unlock() は、mp が指す mutex のロックを解除します。

プロトタイプ:
int	pthread_mutex_unlock(pthread_mutex_t *mp); 
#include <pthread.h>

pthread_mutex_t mp;
int ret;

ret = pthread_ mutex_unlock(&mp); /* mutex を解除する */

mutex はロックされていて、呼び出しスレッドがその mutex を最後にロックしたスレッド (つまり、呼び出しスレッドがその mutex を保持している) でなければなりません。その mutex が使用可能になるのを待っているスレッドが他にある場合は、その mutex ロックに対する待ち行列内の先頭のスレッドのブロックが解除されます。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

mp で指定された値が、初期化された mutex オブジェクトを表していません。

ブロックしないで行う mutex のロック

pthread_mutex_trylock(3T)

pthread_mutex_trylock() は、mp が指す mutex のロックを試みます。

プロトタイプ:
int	pthread_mutex_trylock(pthread_mutex_t *mp); 
#include <pthread.h>

pthread_mutex_t mp;
int ret;

ret = pthread_ mutex_trylock(&mp); /* mutex のロックを試みる */

この関数はブロックしない点を除いて、pthread_mutex_lock() と同じ働きをします。mutex がすでにロックされている場合は、ただちにエラーを返します。mutex がロックされていなければ、呼び出しスレッドがロックを獲得します。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。


EBUSY

mp が指す mutex がすでにロックされています。


EINVAL

mp で指定された値が、初期化された mutex オブジェクトを表していません。

mutex の削除

pthread_mutex_destroy(3T)

pthread_mutex_destroy() は、mp が指す mutex に関連するすべての状態を削除します。

プロトタイプ:
int	pthread_mutex_destroy(pthread_mutex_t *mp); 
#include <pthread.h>

pthread_mutex_t mp;
int ret;

ret = pthread_mutex_destroy(&mp); /* mutex を削除する */

mutex の記憶領域は解放されません。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

mp で指定された値が、初期化された mutex オブジェクトを表していません。

相互排他ロックのコード例

次に、相互排他ロックを示すコードの一部を示します。


例 4-1 相互排他ロックの例

#include <pthread.h>

pthread_mutex_t count_mutex;
long long count;

void
increment_count()
{
	    pthread_mutex_lock(&count_mutex);
    count = count + 1;
	    pthread_mutex_unlock(&count_mutex);
}

long long
get_count()
{
    long long c;
    
    pthread_mutex_lock(&count_mutex);
	    c = count;
    pthread_mutex_unlock(&count_mutex);
	    return (c);
}

例 4-1 の 2 つの関数は、相互排他ロックをそれぞれ別の目的で使用しています。increment_count() 関数は、相互排他ロックによって共有変数の原子的操作による更新を保証しています。get_count() 関数は、相互排他ロックによって 64 ビット値の count が原子的に読み取られるようにしています。32 ビットアーキテクチャでは、long long は実際には 2 つの 32 ビット値として処理されます。

整数はほとんどのマシンで共通のワードサイズであるため、整数値の読み取りは原子的操作です。

ロック階層の使用

同時に 2 つの資源をアクセスすることがあります。一方の資源を使用しているとき、もう一方の資源も必要となる場合があります。例 4-2 は、2 つのスレッドが同じ 2 つの資源を要求しようとして両者が異なる順序で、対応する相互排他ロックを獲得しようとする場合に問題が生じることを示しています。この例では 、2 つのスレッドがそれぞれ mutex の 1 と 2 をロックした場合、次に各スレッドが互いにもう一方の mutex をロックしようとするとデッドロックが発生します。


例 4-2 デッドロック

スレッド 1 

スレッド 2 

pthread_mutex_lock(&m1);

 

/* 資源 1 を使用 */ 

 

pthread_mutex_lock(&m2);

 

/* 資源 1 と 2 を使用 */ 

 

pthread_mutex_unlock(&m2);

pthread_mutex_unlock(&m1);

pthread_mutex_lock(&m2);

 

/* 資源 2 を使用 */ 

 

pthread_mutex_lock(&m1);

 

/* 資源 1 と 2 を使用 */ 

pthread_mutex_unlock(&m1);

pthread_mutex_unlock(&m2);


この問題を回避する最善の方法は、スレッドで複数の mutex をロックする場合、常に同じ順序でロックすることです。この方法をロック階層と呼び、mutex に論理的な番号を割り振ることにより mutex に順序を付けます。

自分がある番号を持つ mutex を保持しているときより小さい番号が割り振られている mutex はロックできないという規定を守るようにします。


注 -

ロック lint ツールを使うと、この例で示したようなデッドロックの問題を検出できます。この種のデッドロック問題を回避する最善の方法は、ロック階層を使用することです。常に一定の順序でロックする限り、デッドロックは発生しません。


ただし、この方法は常に使用できるとは限りません。規定と違う順序で相互排他ロックを獲得しなければならないこともあるからです。そのような状況でデッドロックを防ぐには、pthread_mutex_trylock() を使用します。デッドロックが避けられないような事態が生じた場合は、ある 1 つのスレッドが現在保持している mutex のロックを解除する必要があります。

例 4-3 は、その方法を示しています。


例 4-3 条件付きロック

スレッド 1 

スレッド 2 

pthread_mutex_lock(&m1); pthread_mutex_lock(&m2);

 

 

 

 

/* 解放 */ 

 

pthread_mutex_unlock(&m2);

pthread_mutex_unlock(&m1);

for (; ;) 

{ pthread_mutex_lock(&m2);

 

if (pthread_mutex_trylock(&m1)==0)

/* 獲得成功 */  

break;

/* 獲得失敗 */ 

pthread_mutex_unlock(&m2);

/* ロックを獲得し、解放 */ 

pthread_mutex_unlock(&m1);

pthread_mutex_unlock(&m2);


この例では、スレッド 1 は mutex を規定通りの順序でロックしようとしていますが、スレッド 2 ではロックの順序が違います。デッドロックが発生しないようにするために、スレッド 2 は mutex の 1 を慎重にロックしなければなりません。これは、mutex の 1 が解放されるまで待つとすると、スレッド 1 との間にデッドロックの関係が生じる恐れがあるからです。

これを防ぐため、スレッド 2 は pthread_mutex_trylock() を呼び出し、mutex がロックされていなければロックします。ロックされていれば、スレッド 2 はただちにエラーを返します。その時点で、スレッド 2 は mutex の 2 を解放しなければなりません。その結果、スレッド 1 は mutex の 2 をロックでき、最終的には mutex の 1 と 2 の両方を解放します。

片方向リンクリストの入れ子のロック

例 4-4例 4-5 で、一度に 3 つのロックを獲得する場合を説明します。この例では、デッドロックを防ぐために規定された順序でロックします。


例 4-4 片方向リンクのリスト構造体

typedef struct node1 {
    int value;
    struct node1 *link;
    pthread_mutex_t lock;
} node1_t;

node1_t ListHead;

この例で使用する片方向リンクのリスト構造体は、各ノードに相互排他ロックを含んでいます。このリストから特定のノードを削除する場合は、最初に ListHead (これが削除されることはない) の位置からリストを辿って目的のノードを探します。

この検索を同時並行的に行われる削除から保護するために、各ノードをロックしてからノードの内容をアクセスしなければなりません。すべての検索が ListHead の位置から開始されるので、常にリストの順序でロックされます。このため、デッドロックは決して発生しません。

目的のノードが見つかった時は、この変更がそのノードと直前のノードの両方に影響を与えるため、両方をロックします。直前のノードのロックが常に最初に獲得されるので、ここでもデッドロックの心配はありません。

例 4-5 は、片方向リンクリストから特定のノードを削除する C コードを示しています。


例 4-5 片方向リンクリストの入れ子のロック

node1_t *delete(int value)
{
    node1_t *prev, *current;

    prev = &ListHead;
    pthread_mutex_lock(&prev->lock);
    while ((current = prev->link) != NULL) {
        pthread_mutex_lock(&current->lock);
        if (current->value == value) {
            prev->link = current->link;
            pthread_mutex_unlock(&current->lock);
            pthread_mutex_unlock(&prev->lock);
            current->link = NULL;
            return(current);
        }
        pthread_mutex_unlock(&prev->lock);
        prev = current;
    }
    pthread_mutex_unlock(&prev->lock);
    return(NULL);
}

循環リンクリストの入れ子のロック

例 4-6 は、前述のリスト構造を修正して循環リストにしたものです。先頭のノードとして識別されるノードはありません。スレッドは適当な 1 つのノードに関連付けられると、そのノードと次のノードに対して操作を行います。この状況ではロック階層は適用できません。明らかに階層 (つまり、リンクをたどる順番) が循環的だからです。


例 4-6 循環リンクリスト

typedef struct node2 {
    int value;
    struct node2 *link;
    pthread_mutex_t lock;
} node2_t;

例 4-7 では 2 つのノードをロックし、両方のノードに対してある操作を行なっている C コードを示します。


例 4-7 循環リンクリストの入れ子のロック

void Hit Neighbor(node2_t *me) {
    while (1) {
        pthread_mutex_lock(&me->lock);
        if (pthread_mutex_lock(&me->link->lock)!= 0) {
            /* ロック失敗 */             
            pthread_mutex_unlock(&me->lock);              
            continue;         
        }         
        break;     
    }     
    me->link->value += me->value;     
    me->value /=2;     
    pthread_mutex_unlock(&me->link->lock);     
    pthread_mutex_unlock(&me->lock);
}

条件変数の属性

条件変数は、ある条件が真になるまでスレッドを原子的操作によりブロックしたいときに使用します。必ず相互排他ロックとともに使用してください。

条件変数を使うと、特定の条件が真になるまでスレッドを原子的操作によりブロックできます。この条件判定は、相互排他ロックにより保護された状態で行います。

条件が偽のとき、スレッドは通常は条件変数でブロック状態に入り、相互排他ロックを原子的操作により解除して、条件が変更されるのを待ちます。別のスレッドが条件を変更すると、そのスレッドはそれに関連する条件変数にシグナルを送り、その条件変数でブロックしているスレッドを呼び起こします。呼び起こされたスレッドは再度相互排他ロックを獲得し、条件を再び評価します。

異なるプロセスに所属するスレッドの間で、条件変数を使って同期をとるためには、連携するそれらのプロセスの間で共有される書き込み可能なメモリーに、条件変数の領域を確保する必要があります。

スケジューリング方針は、ブロックされたスレッドがどのように呼び起こさるかを決定します。デフォルト SCHED_OTHER の場合、スレッドは優先順位に従って呼び起こされます。

条件変数の属性は、使用する前に設定して初期化しておかなければなりません。条件変数の属性を操作する関数を表 4-4 に示します。

表 4-4 条件変数の属性

「条件変数の属性の初期化」

「pthread_condattr_init(3T)」

「条件変数の属性の削除」

「pthread_condattr_destroy(3T)」

「条件変数のスコープの設定」

「pthread_condattr_setpshared(3T)」

「条件変数のスコープの取得」

「pthread_condattr_getpshared(3T)」

条件変数のスコープ定義について、オリジナルの Solaris スレッドとの相違点を表 4-5 に示します。

表 4-5 条件変数のスコープの比較

Solaris 

POSIX 

定義 

USYNC_PROCESS

PTHREAD_PROCESS_SHARED

このプロセスと他のプロセスのスレッドの間で同期をとるために使用する。 

USYNC_THREAD

PTHREAD_PROCESS_PRIVATE

このプロセスのスレッドの間でだけ同期をとるために使用する。 

条件変数の属性の初期化

pthread_condattr_init(3T)

pthread_condattr_init() は、このオブジェクトに関連付けられた属性をデフォルト値に初期化します。各属性オブジェクトのための記憶領域は、実行時にスレッドシステムによって割り当てられます。この関数が呼び出されたときの pshared 属性のデフォルト値は PTHREAD_PROCESS_PRIVATE で、初期化された条件変数を 1 つのプロセスの中だけで使用できるという意味です。

プロトタイプ:
int	pthread_condattr_init(pthread_condattr_t *cattr);
#include pthread.h
pthread_condattr_t  cattr;
int ret;

/* 属性をデフォルト値に初期化する */
ret = pthread_condattr_init(&cattr); 

cattr は不透明なデータ型で、システムによって割り当てられた属性オブジェクトを格納します。cattr のスコープとして取りうる値は、PTHREAD_PROCESS_PRIVATE (デフォルト) と PTHREAD_PROCESS_SHARED です。

条件変数属性を再使用するには、pthread_condattr_destroy(3T) によって事前に削除しなければなりません。pthread_condattr_init() 呼び出しは、不透明なオブジェクトへのポインタを戻します。そのオブジェクトが削除されないと、結果的にメモリーリークを引き起こします。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を戻します。


ENOMEM

メモリーが足りなくて、スレッド属性オブジェクトを初期化できません。


EINVAL

cattr で指定された値が無効です。

条件変数の属性の削除

pthread_condattr_destroy(3T)

このルーチンは記憶領域を解除し、属性オブジェクトを無効にします。

プロトタイプ:
int	pthread_condattr_destroy(pthread_condattr_t *cattr);
#include <pthread.h>
pthread_condattr_t cattr;
int ret;

/* 属性を削除する */
ret
 = pthread_condattr_destroy(&cattr); 

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

cattr で指定された値が無効です。

条件変数のスコープの設定

pthread_condattr_setpshared(3T)

pthread_condattr_setpshared() は、プロセス専用 (プロセス内) とシステム共通 (プロセス間) のどちらかに条件変数のスコープを設定します。pshared 属性を PTHREAD_PROCESS_SHARED 状態に設定して条件変数を生成し、その条件変数が共有メモリー内に存在する場合、その条件変数は複数のプロセスのスレッドの間で共有できます。これは、オリジナルの Solaris スレッドにおいて mutex_init()USYNC_PROCESS フラグを使用するのに相当します。

mutex の pshared 属性を PTHREAD_PROCESS_PRIVATE (デフォルト値) に設定した場合、その mutex を操作できるのは同じプロセスで生成されたスレッドに限られます。PTHREAD_PROCESS_PRIVATE を使用した場合、その動作はオリジナルの Solaris スレッドにおいて cond_init() 呼び出しで USYNC_THREAD フラグを使用したとき、すなわち局所条件変数と同じになります。PTHREAD_PROCESS_SHARED は広域条件変数に相当します。

プロトタイプ:
int	pthread_condattr_setpshared(pthread_condattr_t *cattr,
    int pshared);
#include <pthread.h>

pthread_condattr_t cattr;
int ret;

/* 全プロセス */
ret = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);

/* 1 つのプロセス内 */
ret = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_PRIVATE);

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

cattr または pshared の値が無効です。

条件変数のスコープの取得

pthread_condattr_getpshared(3T)

pthread_condattr_getpshared() は属性オブジェクト cattrpshared の現在のスコープ値を取得します。これは PTHREAD_PROCESS_SHAREDPTHREAD_PROCESS_PRIVATE のどちらかです。

プロトタイプ:
int	pthread_condattr_getpshared(const pthread_condattr_t *cattr,
    int *pshared);
#include <pthread.h>

pthread_condattr_t cattr;
int pshared;
int ret;

/* 条件変数の pshared 値を取得する */
ret = pthread_condattr_getpshared(&cattr, &pshared); 

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

cattr の値が無効です。

条件変数の使用方法

この節では条件変数の使用方法を説明します。表 4-6 にそのための関数を示します。

表 4-6 条件変数関数

「条件変数の初期化」

「pthread_cond_init(3T)」

「条件変数によるブロック」

「pthread_cond_wait(3T)」

「特定のスレッドのブロック解除」

「pthread_cond_signal(3T)」

「時刻指定のブロック」

「pthread_cond_timedwait(3T)」

「全スレッドのブロック解除」

「pthread_cond_broadcast(3T)」

「条件変数の削除」

「pthread_cond_destroy(3T)」

条件変数の初期化

pthread_cond_init(3T)

pthread_cond_init() は、cv が指す条件変数をデフォルト値 (cattr が NULL) に初期化します。また、pthread_condattr_init() ですでに設定してある条件変数の属性を指定することもできます。cattr を NULL にするのは、デフォルト条件変数属性オブジェクトのアドレスを渡すのと同じですが、メモリーのオーバーヘッドがありません。

プロトタイプ:
int	pthread_cond_init(pthread_cond_t *cv,
    const pthread_condattr_t *cattr);
#include <pthread.h>

pthread_cond_t cv;
pthread_condattr_t cattr;
int ret;

/* 条件変数をデフォルト値に初期化する */
ret = pthread_cond_init(&cv, NULL);

/* 条件変数を初期化する */
ret = pthread_cond_init(&cv, &cattr); 

静的に定義された条件変数は、マクロ PTHREAD_COND_INITIALIZER で、デフォルト属性をもつように直接初期化できます。この効果は、NULL 属性を指定して pthread_cond_init() を動的に割り当てるのと同じです。エラーチェックは行われません。

複数のスレッドで同じ条件変数を同時に初期化または再初期化しないでください。条件変数を再初期化または削除する場合、アプリケーションでその条件変数が現在使用されていないことを確認しなければなりません。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を戻します。


EINVAL

cattr で指定された値が無効です。


EBUSY

その条件変数は現在使用されています。


EAGAIN

必要な資源が利用できません。


ENOMEM

メモリーが足りないため条件変数を初期化できません。

条件変数によるブロック

pthread_cond_wait(3T)

pthread_cond_wait() は、mp が指す相互排他ロックを原子的操作により解放し、cv が指す条件変数で呼び出しスレッドをブロックします。

プロトタイプ:
int	pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);
#include <pthread.h>

pthread_cond_t cv;
pthread_mutex_t mp;
int ret;

/* 条件変数でブロック */
ret = pthread_cond_wait(&cv, &mp); 

ブロックされたスレッドを呼び起こすには、pthread_cond_signal()pthread_cond_broadcast() を使います。また、スレッドはシグナルの割り込みによっても呼び起こされます。

pthread_cond_wait() が戻ったからといって、条件変数に対応する条件の値が変化したと判断することはできません。このため、条件をもう一度評価しなければなりません。

pthread_cond_wait() が戻るときは、たとえエラーを戻したときでも、常に mutex は呼び出しスレッドがロックし保持している状態にあります。

pthread_cond_wait() は、指定の条件変数にシグナルが送られてくるまでブロック状態になります。pthread_cond_wait() は原子的操作により、対応する mutex ロックを解除してからブロック状態に入り、ブロック状態から戻る前にもう一度原子的操作によりロックを獲得します。

通常の用法は次のとおりです。mutex ロックの保護下で条件式を評価します。条件式が偽のとき、スレッドは条件変数でブロック状態に入ります。別のスレッドが条件の値を変更すると、条件変数にシグナルが送られます。その条件変数でブロックされていた (1 つまたは全部の) スレッドは、そのシグナルによってブロックが解除され、もう一度 mutex ロックを獲得しようとします。

呼び起こされたスレッドが pthread_cond_wait() から戻る前に条件が変更されることもあるので、mutex ロックを獲得する前に、待ち状態の原因となった条件をもう一度評価しなければなりません。条件チェックを while() ループに入れ、そこで pthread_cond_wait() を呼び出すようにすることをお勧めします。

    pthread_mutex_lock();
        while(condition_is_false)
            pthread_cond_wait();
    pthread_mutex_unlock();

条件変数で複数のスレッドがブロックされているとき、それらのスレッドが、どの順番でブロックが解除されるかは不定です。


注 -

pthread_cond_wait() は取り消しポイントです。保留状態になっている取り消しがあって、呼び出しスレッドが取り消しを有効 (使用可能) にしている場合、そのスレッドは終了し、ロックしている間にクリーンアップハンドラの実行を開始します。


戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

cv または mp で指定された値が無効です。

特定のスレッドのブロック解除

pthread_cond_signal(3T)

pthread_cond_signal() は、cv が指す条件変数でブロックされている 1 つのスレッドのブロックを解除します。

プロトタイプ:
int	pthread_cond_signal(pthread_cond_t *cv);
#include <pthread.h>

pthread_cond_t cv;
int ret;

/* ある条件変数がシグナルを送る */
ret = pthread_cond_signal(&cv); 

pthread_cond_signal() は、シグナルを送ろうとしている条件変数で使用されたものと同じ mutex ロックを獲得した状態で呼び出してください。そうしないと、関連する条件が評価されてから pthread_cond_wait() でブロック状態に入るまでの間に、条件変数にシグナルが送られる可能性があり、その場合 pthread_cond_wait() は永久に待ち続けることになります。

スケジューリング方針は、ブロックされたスレッドがどのように呼び起こされるかを決定します。SCHED_OTHER の場合、スレッドは優先順位に従って呼び起こされます。

スレッドがブロックされていない条件変数に対して pthread_cond_signal() を実行しても無視されます。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

cv が指すアドレスが正しくありません。


例 4-8 pthread_cond_wait()pthread_cond_signal() の使用例

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;

decrement_count()
{
    pthread_mutex_lock(&count_lock);
    while (count == 0)
        pthread_cond_wait(&count_nonzero, &count_lock);
    count = count - 1;
    pthread_mutex_unlock(&count_lock);
}

increment_count()
{
    pthread_mutex_lock(&count_lock);
    if (count == 0)
        pthread_cond_signal(&count_nonzero);
    count = count + 1;
    pthread_mutex_unlock(&count_lock);
}

時刻指定のブロック

pthread_cond_timedwait(3T)

プロトタイプ:
int	pthread_cond_timedwait(pthread_cond_t *cv,
    pthread_mutex_t *mp, const struct timespec *abstime);
#include <pthread.h>
#include <time.h>

pthread_cond_t cv;
pthread_mutex_t mp;
timestruct_t abstime;
int ret;

/* 条件変数で指定した時刻までブロック */
ret = pthread_cond_timedwait(&cv, &mp, &abstime); 

pthread_cond_timedwait() は、abstime で指定した時刻を過ぎるとブロック状態を解除する点を除いて、pthread_cond_wait() と同じ動作をします。pthread_cond_timedwait() が戻るときは、たとえエラーを戻したときでも、常に mutex は呼び出しスレッドがロックして保持している状態です。

pthread_cond_timedwait() のブロック状態が解除されるのは、条件変数にシグナルが送られてきたときか、一番最後の引数で指定した時刻を過ぎたときです。


注 -

pthread_cond_timedwait() は、取り消しポイントでもあります。


戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

cv または abstime が不当なアドレスを指しています。


ETIMEDOUT

abstime で指定された時刻を過ぎています。

時間切れの指定は時刻で行うため、時間切れ時刻を再計算する必要がないので、効率的に条件を再評価できます (詳細は、例 4-9 を参照してください)。


例 4-9 時刻指定のブロック

pthread_timestruc_t to;
pthread_mutex_t m;
pthread_cond_t c;
...
pthread_mutex_lock(&m);
to.tv_sec = time(NULL) + TIMEOUT;
to.tv_nsec = 0;
while (cond == FALSE) {
    err = pthread_cond_timedwait(&c, &m, &to);
    if (err == ETIMEDOUT) {
        /* 時間切れの場合の処理 */
        break;
    }
}
pthread_mutex_unlock(&m);

全スレッドのブロック解除

pthread_cond_broadcast(3T)

プロトタイプ:
int	pthread_cond_broadcast(pthread_cond_t *cv);
#include <pthread.h>

pthread_cond_t cv;
int ret;

/* 条件変数すべてがシグナルを受ける */
ret = pthread_cond_broadcast(&cv); 

pthread_cond_broadcast() は、cv (pthread_cond_wait() で指定された) が指す条件変数でブロックされている、すべてのスレッドのブロックを解除します。スレッドがブロックされていない条件変数に対して pthread_cond_broadcast() を実行しても無視されます。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

cv が指すアドレスが正しくありません。

条件変数に対するブロードキャストの例

条件変数でブロックされていたすべてのスレッドが、もう一度相互排他ロックを争奪するようになるので慎重に使用してください。たとえば、pthread_cond_broadcast() を使用すると可変量の資源に対して、その資源が解放される時にスレッド間で争奪させることができます (詳細は、例 4-10 を参照してください)。


例 4-10 条件変数に対するブロードキャスト

pthread_mutex_t rsrc_lock;
pthread_cond_t rsrc_add;
unsigned int resources;

get_resources(int amount)
{
    pthread_mutex_lock(&rsrc_lock);
    while (resources < amount) {
        pthread_cond_wait(&rsrc_add, &rsrc_lock);
    }
    resources -= amount;
    pthread_mutex_unlock(&rsrc_lock);
}

add_resources(int amount)
{
    pthread_mutex_lock(&rsrc_lock);
    resources += amount;
    pthread_cond_broadcast(&rsrc_add);
    pthread_mutex_unlock(&rsrc_lock);
}

上記のコード例の add_resources() で、次の点に注意してください。相互排他ロックの範囲内では、resources の更新と pthread_cond_broadcast() の呼び出しはどちらを先に行なってもかまいません。

pthread_cond_broadcast() は、シグナルを送ろうとしている条件変数で使用されたのと同じ相互排他ロックを獲得した状態で呼び出してください。そうしないと、関連する条件変数が評価されてから pthread_cond_wait() でブロック状態に入るまでの間に条件変数にシグナルが送られる可能性があり、その場合 pthread_cond_wait() は永久に待ち続けることになります。

条件変数の削除

pthread_cond_destroy(3T)

pthread_cond_destroy() は、cv が指す条件変数を削除します。

プロトタイプ:
int	pthread_cond_destroy(pthread_cond_t *cv);
#include <pthread.h>

pthread_cond_t cv;
int ret;

/* 条件変数を削除する */
ret = pthread_cond_destroy(&cv); 

条件変数の記憶領域は解放されません。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

cv で指定された値が無効です。

「呼び起こし忘れ」問題

pthread_cond_signal() または pthread_cond_broadcast() を呼び出すとき、スレッドが条件変数に関連する相互排他ロックを保持していないと「呼び起こし忘れ」(lost wake-up) という問題が生じることがあります。

次のすべての条件に該当する場合は、そのシグナルには効果がないので「呼び起こし忘れ」が発生します。

「生産者 / 消費者」問題

「生産者 / 消費者」問題は、並行プログラミングに関する問題の中でも一般によく知られているものの 1 つです。この問題は次のように定式化されます。サイズが有限の 1 個のバッファと 2 種類のスレッドが存在します。一方のスレッドを生産者、もう一方のスレッドを消費者と呼びます。生産者がバッファにデータを入れ、消費者がバッファからデータを取り出します。

生産者は、バッファに空きができるまでデータを入れることができません。消費者は、バッファが空の間はデータを取り出すことができません。

特定の条件のシグナルを待つスレッドの待ち行列を条件変数で表すことにします。

例 4-11 では、そうした待ち行列として lessmore の 2 つを使用しています。less はバッファ内の未使用スロットを待つ生産者のための待ち行列で、more は情報が格納されたバッファスロットを待つ消費者のための待ち行列です。また、バッファが同時に複数のスレッドによってアクセスされないようにするために、相互排他ロック (mutex ロック) も使用しています。


例 4-11 「生産者 / 消費者」問題と条件変数

typedef struct {
    char buf[BSIZE];
    int occupied;
    int nextin;
    int nextout;
    pthread_mutex_t mutex;
    pthread_cond_t more;
    pthread_cond_t less;
} buffer_t;

buffer_t buffer;

例 4-12 は、生産者側の処理です。最初に、mutex をロックしてバッファデータ構造 (buffer) を保護します。次に、これから作成するデータのための空きがあるか確認します。空きがない場合は、pthread_cond_wait() を呼び出して、「バッファ内に空きがある」を表す条件 less にシグナルが送られてくるのを待つスレッドの待ち行列に入ります。

同時に、pthread_cond_wait() の呼び出しによって、スレッドは mutex のロックを解除します。生産者スレッドは、条件が真になって消費者スレッドがシグナルを送ってくれるのを待ちます (詳細は、例 4-12 を参照してください)。条件にシグナルが送られてくると、less を待っている一番目のスレッドが呼び起こされます。しかし、そのスレッドは pthread_cond_wait() が戻る前に、mutex ロックを再び獲得する必要があります。

このようにして、バッファデータ構造への相互排他アクセスが保証されます。その後、生産者スレッドはバッファに本当に空きがあるか確認しなければなりません。空きがある場合は、最初の未使用スロットにデータを入れます。

このとき、バッファにデータが入れられるのを消費者スレッドが待っている可能性があります。そのスレッドは、条件変数 more で待ち状態となっています。生産者スレッドはバッファにデータを入れると、pthread_cond_signal() を呼び出して、待ち状態の最初の消費者を呼び起こします (待ち状態の消費者がいないときは、この呼び出しは無視されます)。

最後に、生産者スレッドは mutex ロックを解除して、他のスレッドがバッファデータ構造を操作できるようにします。


例 4-12 「生産者 / 消費者」問題 − 生産者

void producer(buffer_t *b, char item)
{
    pthread_mutex_lock(&b->mutex);
   
    while (b->occupied >= BSIZE)
        pthread_cond_wait(&b->less, &b->mutex);

    assert(b->occupied < BSIZE);

    b->buf[b->nextin++] = item;

    b->nextin %= BSIZE;
    b->occupied++;

    /* 現在の状態:  「b->occupied < BSIZE かつ b->nextin はバッファ内
       の次の空きスロットのインデックス」または「b->occupied == BSIZE 
       かつ b->nextin は次の (占有されている) スロットのインデックス。これは
       消費者によって空にされる (例: b->nextin == b->nextout) 」 */

    pthread_cond_signal(&b->more);

    pthread_mutex_unlock(&b->mutex);
}

上記のコード例の assert() 文の用法に注意してください。コンパイル時に NDEBUG を定義しなければ、assert() は次のように動作します。すなわち、引数が真 (0 以外の値) のときは何も行わず、引数が偽 (0) のときはプログラムを強制的に終了させます。このように、実行時に発生した問題をただちに指摘できる点がマルチスレッドプログラムに特に適しています。assert() はデバッグのための有用な情報も与えてくれます。

/* 現在の状態: ... で始まるコメント部分も assert() で表現した方がよいかもしれません。しかし、論理式で表現するには複雑すぎるので、ここでは文章で表現しています。

上記の assert() やコメント部分の論理式は、どちらも不変式の例です。不変式は、あるスレッドが不変式中の変数を変更している瞬間を除いて、プログラムの実行により偽の値に変更されない論理式です (もちろん assert() の論理式は、どのスレッドがいつ実行した場合でも常に真であるべきです)。

不変式は非常に重要な手法です。プログラムテキストとして明示的に表現しなくても、プログラムを分析するときは不変式に置き換えて問題を考えることが大切です。

上記の生産者コード内のコメントで表現された不変式は、スレッドがそのコメントを含むコード部分を処理中には常に真となります。しかし、それを mutex_unlock() のすぐ後ろに移動すると、必ずしも常に真とはなりません。assert() のすぐ後ろに移動した場合は、真となります。

つまり、この不変式は、生産者または消費者がバッファの状態を変更しようとしているとき以外は、常に真となるような特性を表現しています。スレッドは mutex の保護下でバッファを操作しているとき、この不変式の値を一時的に偽にしてもかまいません。しかし、処理が完了したら不変式の値を再び真に戻さなければなりません。

例 4-13 は、消費者の処理です。この処理の流れは生産者の場合と対称的です。


例 4-13 「生産者 / 消費者」問題 − 消費者

char consumer(buffer_t *b)
{
    char item;
    pthread_mutex_lock(&b->mutex);
    while(b->occupied <= 0)
        pthread_cond_wait(&b->more, &b->mutex);

    assert(b->occupied > 0);

    item = b->buf[b->nextout++];
    b->nextout %= BSIZE;
    b->occupied--;

    /* 現在の状態:  「b->occupied > 0 かつ b->nextout はバッファ内の
       最初の占有されているスロットのインデックス」または「b->occupied == 0 
       かつ b-> nextout は次の (未使用) スロットのインデックス。これは生産者側
       によっていっぱいにされる (例: b->nextout == b->nextin) 」 */

    pthread_cond_signal(&b->less);
    pthread_mutex_unlock(&b->mutex);

    return(item);
}

セマフォ

セマフォは、E.W. ダイクストラ (Dijkstra) が 1960 年代の終わりごろに考案したプログラミング手法です。ダイクストラのセマフォモデルは、鉄道線路の運行をモデル化したものです。一度に一本の列車しか走れない単線の鉄道線路を思い浮かべてください。

この鉄道線路を保護するのがセマフォです。列車は単線区間に入るとき、セマフォの状態が進行許可状態になるのを待たなければなりません。列車が単線区間に入るとセマフォの状態は、他の列車が単線区間に入るのを禁止する状態に変化します。単線区間から出る列車は、セマフォの状態を進行許可状態に戻して他の列車が単線区間に入ることができるようにしなければなりません。

コンピュータ内のセマフォは、単一の整数で表現されます。スレッドは進行が許可されるのを待ち、その後進行したことを知らせるためにセマフォに対して P 操作を実行します。

この操作をもう少し具体的に説明しましょう。スレッドは、セマフォの値が正になるのを待たなければなりません。その後 1 を引くことでセマフォの値を変更します。これが P 操作です。処理を完了したセマフォは、V 操作を実行します。この操作は 1 を加えることでセマフォの値を変更します。ここで必ず守らなければならないことがあります。これらの各操作を原子的操作により行うことです。これは操作が分断されると、操作途中にセマフォに対する別の操作が行われる危険性があるからです。P 操作では、1 を引く直前のセマフォの値が正でなければなりません (結果的に、引いた後の値が負にならないことと、その値が引く前の値よりも 1 だけ小さいことが保証されます)。

P 操作と V 操作のどちらの演算操作でも干渉が生じないようにしなければなりません。たとえば、同じセマフォに対して 2 つの V 操作が同時に行われた場合、そのセマフォの新しい値は最初よりも 2 だけ大きくなっていなければなりません。

ダイクストラがオランダ人だったこともあり、P と V の記号的な意味は現在ではほとんど忘れられています。参考までに、P はオランダ語の「prolagen」という単語を表します。その語源は「proberen te verlagen」で、「小さくする」という意味です。また、V は「verhogen」を表し、「大きくする」という意味です。このことは、ダイクストラのテクニカルノート『EWD 74』で説明しています。

sema_wait(3R)sema_post(3R) は、ダイクストラの P 操作と V 操作にそれぞれ対応しています。また、sema_trywait(3R) は、P 操作の条件付きの形式です。この関数は、呼び出しスレッドがセマフォの値を差し引くために待たなければならない場合は、ただちに 0 以外の値を返します。

セマフォは、2 進セマフォとカウント用セマフォの 2 種類に大別されます。2 進セマフォは 0 と 1 のどちらかの値しかとりません。一方、カウント用セマフォは負以外の任意の値をとることができます。2 進セマフォは、論理的には相互排他ロック (mutex ロック) と似ています。

必須要件ではありませんが、mutex はロックを保持しているスレッドだけがそのロックを解放すべきものです。一方、セマフォには「スレッドがセマフォを保持している」という概念がないので、どのスレッドも V 操作 (すなわち、sem_post(3R)) を実行できます。

カウント用セマフォは、mutex とともに使用される条件変数と同等の能力があります。多くの場合、条件変数よりもカウント用セマフォを使用した方がコードが簡素化されます (詳細は、後述の例を参照してください)。

mutex とともに条件変数を使用する場合は、プログラムのどの部分を保護するかが自然な形で明らかになりました。ところが、セマフォでは必ずしもそうはなりません。強力だからといって安易に使うとプログラムが不統一で理解しにくくなります。このため、「並行プログラミングにおける goto」と呼ばれています。

計数型セマフォ

セマフォは、負の値をとらない整数のカウンタと考えることができます。通常は、資源に対するアクセスの調整をはかる目的で、次のように使用されます。最初に、使用可能な資源の数をセマフォに初期設定します。その後、スレッドは資源が追加されるときにセマフォの値を原子的操作によって 1 増やし、資源が削除されるときに原子的操作によって 1 減らします。

セマフォの値が 0 になった場合は、資源がないことを意味します。この場合、セマフォの値を 1 減らそうとすると、スレッドはセマフォの値が 0 より大きくなるまでブロックされます。

表 4-7 セマフォに関するルーチン

「セマフォの初期化」

「sem_init(3R)」

「セマフォの加算」

「sem_post(3R)」

「セマフォの値によるブロック」

「sem_wait(3R)」

「セマフォの減算」

「sem_trywait(3R)」

「セマフォの削除」

「sem_destroy(3R)」

セマフォは、その獲得と解放を同じスレッドで行う必要がないため、シグナルハンドラで行われているような非同期のイベント通知を実現できます。また、セマフォ自身が状態を持っているため、条件変数を使用する場合と違って相互排他ロックを獲得しなくても非同期で使用できます。ただし、セマフォは相互排他ロックほど効率的ではありません。

セマフォで複数のスレッドがブロックされているとき、それらのスレッドがどの順番でブロック解除されるかは、特に指定しなければ不定です。

セマフォは、使用する前に初期化されている必要がありますが、属性はありません。

セマフォの初期化

sem_init(3R)

プロトタイプ:
int	sem_init(sem_t *sem, int pshared, unsigned int value);
#include <semaphore.h>

sem_t sem;
int pshared;
int ret;
int value;

/* セマフォの初期化 */
pshared = 0;
value = 1;
ret = sem_init(&sem, pshared, value); 

sem_init() は、sem が指すセマフォ変数を value の値に初期設定します。pshared の値が 0 なら、そのセマフォはプロセス間で共有できません。pshared の値が 0 以外なら、そのセマフォはプロセス間で共有できます。

複数のスレッドから同じセマフォを初期化してはいけません。

また、一度初期化したセマフォは、他のスレッドで使用されている可能性があるので再初期化してはいけません。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件のいずれかが検出されると、この関数は失敗し、次の値を戻します。


EINVAL

value の値が SEM_VALUE_MAX を超えています。


ENOSPC

そのセマフォを初期化するのに必要な資源が使い果たされています。セマフォの制限 SEM_NSEMS_MAX に達しています。


EPERM

そのセマフォを初期化するのに必要な特権をそのプロセスがもっていません。

プロセス間スコープでセマフォを初期化する

pshared の値が 0 の場合は、そのプロセス内のスレッドだけがそのセマフォを使用できます。

#include <semaphore.h>

sem_t sem;
int ret;
int count = 4;

/* このプロセスでのみ使用 */
ret = sem_init(&sem, 0, count); 

プロセス間スコープでセマフォを初期化する

pshared の値が 0 以外の場合は、他のプロセスによってそのセマフォは共有されます。

#include <semaphore.h>

sem_t sem;
int ret;
int count = 4;

/* プロセス間で共有 */
ret = sem_init(&sem, 1, count);

名前付きセマフォ

sem_open(3R)sem_getvalue(3R)sem_close(3R)sem_unlink(3R) の各関数が、名前付きセマフォを開く、取得する、閉じる、削除するのにそれぞれ使用できます。sem_open() では、ファイルシステムの名前空間で名前が定義されたセマフォを生成できます。

名前付きセマフォはプロセス間で共有されるセマフォに似ていますが、pshared 値ではなくパス名で参照される点が異なります。

名前付きセマフォの詳細は、sem_open(3R)sem_getvalue(3R)sem_close(3R)sem_unlink(3R) のマニュアルページを参照してください。

セマフォの加算

sem_post(3R)

プロトタイプ:
int	sem_post(sem_t *sem);
#include <semaphore.h>

sem_t sem;
int ret;

ret = sem_post(&sem); /* セマフォを加算する */

sem_post() は、sem が指すセマフォの値を原子的操作によって 1 増やします。そのセマフォでブロックされているスレッドがある場合は、そのスレッドのうちの 1 つのスレッドがブロック解除されます。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

sem が指すアドレスが正しくありません。

セマフォの値によるブロック

sem_wait(3R)

プロトタイプ:
int	sem_wait(sem_t *sem);
#include <semaphore.h>

sem_t sem;
int ret;

ret = sem_wait(&sem); /* セマフォの値の変化を待つ */

sem_wait() は、sem が指すセマフォの値が 0 より大きくなるまでスレッドをブロックし、0 より大きくなったらセマフォの値を原子的操作によって 1 減らします。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

sem が不当なアドレスを指しています。


EINTR

シグナルによってこの関数が割り込まれました。

セマフォの減算

sem_trywait(3R)

プロトタイプ:
int	sem_trywait(sem_t *sem);
#include <semaphore.h>

sem_t sem;
int ret;

ret = sem_trywait(&sem); /* セマフォの値の変化を待つ */

sem_trywait() は、sem が指すセマフォの値が 0 より大きい場合は原子的操作によって 1 減らします。この関数はブロックしない点を除いて、sem_wait() と同じ働きをします。つまり、失敗した場合にはすぐに戻ります。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

sem が不当なアドレスを指しています。


EINTR

シグナルによってこの関数が割り込まれました。


EAGAIN

そのセマフォはすでにロックされているので、sem_trywait() でただちにロックできません。

セマフォの削除

sem_destroy(3R)

プロトタイプ:
int	sem_destroy(sem_t *sem);
#include <semaphore.h>

sem_t sem;
int ret;

ret = sem_destroy(&sem); /* セマフォを削除する */

sem_destroy() は、sem が指すセマフォを削除します。セマフォの記憶領域は解放されません。

戻り値

正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。


EINVAL

sem が指すアドレスが正しくありません。

「生産者 / 消費者」問題 − セマフォを使った例

例 4-14 のデータ構造は、条件変数による「生産者 / 消費者」問題のコード例 (例 4-11 参照) のデータ構造と似ています。2 つのセマフォでそれぞれ、バッファの使用済スロット数と未使用スロット数を表します。これらのセマフォは、未使用スロットができるまで生産者を待たせ、使用済スロットができるまで消費者を待たせます。


例 4-14 「生産者 / 消費者」問題 − セマフォを使った例

typedef struct {
    char buf[BSIZE];
    sem_t occupied;
    sem_t empty;
    int nextin;
    int nextout;
    sem_t pmut;
    sem_t cmut;
} buffer_t;

buffer_t buffer;

sem_init(&buffer.occupied, 0, 0);
sem_init(&buffer.empty,0, BSIZE);
sem_init(&buffer.pmut, 0, 1);
sem_init(&buffer.cmut, 0, 1);

buffer.nextin = buffer.nextout = 0;

ここでは、もう一組の (バイナリ) セマフォを使用しています。これは 2 値型セマフォで、相互排他ロック (mutex ロック) と同じ働きをします。この 2 つのセマフォは、複数の生産者と複数の未使用スロットが存在する場合と、複数の消費者と複数の使用済みスロットが存在する場合に、バッファへのアクセスを制御します。本来このような場合では mutex を使用すべきですが、セマフォの使用例を示すために特に使用しています。


例 4-15 「生産者 / 消費者」問題 − 生産者

void producer(buffer_t *b, char item) {
    sem_wait(&b->empty);
   sem_wait(&b->pmut);

    b->buf[b->nextin] = item;
    b->nextin++;
    b->nextin %= BSIZE;

    sem_post(&b->pmut);
   sem_post(&b->occupied);
}


例 4-16 「生産者 / 消費者」問題 − 消費者

char consumer(buffer_t *b) {
    char item;

    sem_wait(&b->occupied);
   
    sem_wait(&b->cmut);

    item = b->buf[b->nextout];
    b->nextout++;
    b->nextout %= BSIZE;

    sem_post(&b->cmut);

    sem_post(&b->empty);

    return(item);
}

プロセスの境界を越えた同期

今までに説明した同期プリミティブは、プロセスの境界を越えて使用するように設定できます。具体的には次のようにします。まず、その同期変数の領域が共有メモリーに確保されるようにし、次に該当する init() ルーチンを呼び出しますが、これはそのプリミティブをプロセス間共有属性で初期化した後に行います。

「生産者 / 消費者」問題の例

例 4-17 は、前述の「生産者 / 消費者」問題の生産者と消費者をそれぞれ別のプロセスで表現したものです。メインルーチンは、0 に初期化されたメモリーを自分のアドレス空間にマッピングし、子プロセスと共有します。

子プロセスが 1 つ生成され、消費者の処理が実行されます。親プロセスは生産者の処理を実行します。

この例では、生産者と消費者を呼び出す各駆動ルーチンも示しています。producer_driver()stdin から文字を読み込み、producer() を呼び出します。consumer_driver()consumer() を呼び出して文字を受け取り、stdout に書き出します。

例 4-17 のデータ構造は、条件変数による「生産者 / 消費者」の例のデータ構造 (例 4-4 を参照) と同じです。2 つのセマフォでそれぞれ、いっぱいになったバッファ数と未使用バッファ数を表します。これらのセマフォは、未使用バッファができるまで生産者を待たせ、バッファがいっぱいになるまで消費者を待たせます。


例 4-17 プロセスの境界を越えた同期の例

main() {
    int zfd;
    buffer_t *buffer;
    pthread_mutexattr_t mattr;
    pthread_condattr_t cvattr_less, cvattr_more;

    zfd = open("/dev/zero", O_RDWR);
    buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t),
        PROT_READ|PROT_WRITE, MAP_SHARED, zfd, 0);
    buffer->occupied = buffer->nextin = buffer->nextout = 0;

    pthread_mutex_attr_init(&mattr);
    pthread_mutexattr_setpshared(&mattr,
        PTHREAD_PROCESS_SHARED);

    pthread_mutex_init(&buffer->lock, &mattr);
    pthread_condattr_init(&cvattr_less);
    pthread_condattr_setpshared(&cvattr_less, PTHREAD_PROCESS_SHARED);
    pthread_cond_init(&buffer->less, &cvattr_less);
    pthread_condattr_init(&cvattr_more);
    pthread_condattr_setpshared(&cvattr_more,   
        PTHREAD_PROCESS_SHARED);
    pthread_cond_init(&buffer->more, &cvattr_more);

    if (fork() == 0)
        consumer_driver(buffer);
    else
        producer_driver(buffer);
}

void producer_driver(buffer_t *b) {
    int item;

    while (1) {
        item = getchar();
        if (item == EOF) {
            producer(b, `¥0');
            break;
        } else
            producer(b, (char)item);
    }
}

void consumer_driver(buffer_t *b) {
    char item;

    while (1) {
        if ((item = consumer(b)) == '¥0')
            break;
        putchar(item);
    }
}

スレッドライブラリによらないプロセス間ロック

一般的には推奨できる方法ではありませんが、Solaris スレッドでスレッドライブラリを使用せずにプロセス間ロックを行うことも可能です。詳細は、「プロセス間での LWP の使用」を参照してください。

プリミティブの比較

スレッドで使われる最も基本的な同期プリミティブは、相互排他ロックです。相互排他ロックは、メモリー使用量と実行時間の両面で最も効率的な機構です。相互排他ロックの主要目的は、資源へのアクセスを直列化することです。

相互排他ロックについで効率的なプリミティブは、条件変数です。条件変数の主要目的は、状態の変化に基づいてスレッドをブロックすることです。つまり、スレッド待ち機能の提供です。条件変数でスレッドをブロックする場合は、その前に相互排他ロックを獲得しなければなりません。また、pthread_cond_wait() から戻った後に相互排他ロックを解除しなければいけません。また、対応する pthread_cond_signal() 呼び出しまで状態の変更が行われる間、相互排他ロックを保持しておかなければなりません。

セマフォは、条件変数より多くのメモリーを消費しますが、状況によっては条件変数よりも簡単に使用できます。セマフォ変数は、制御でなく状態に基づいて機能するからです。また、ロックのように保持するという概念もありません。スレッドをブロックしているセマフォに対して、どのスレッドもセマフォの値を 1 増やすことができます。