この節では条件変数の使用方法を説明します。表 4–5 に、使用可能な関数を示します。
表 4–5 条件変数関数
操作 |
参照先 |
---|---|
条件変数の初期化 | |
条件変数によるブロック | |
特定のスレッドのブロック | |
時刻指定のブロック | |
間隔指定のブロック | |
全スレッドのブロック解除 | |
条件変数の削除 |
cv が指す条件変数をデフォルト値に初期化する場合や、すでに pthread_condattr_init() で設定されている条件変数を指定する場合は、pthread_cond_init(3C) を使用します。
int pthread_cond_init(pthread_cond_t *restrict cv, const pthread_condattr_t *restrict cattr);
#include <pthread.h> pthread_cond_t cv; pthread_condattr_t cattr; int ret; /* initialize a condition variable to its default value */ ret = pthread_cond_init(&cv, NULL); /* initialize a condition variable */ ret = pthread_cond_init(&cv, &cattr);
cattr を NULL に設定するのは、デフォルト条件変数属性オブジェクトのアドレスを渡すのと同じですが、メモリーのオーバーヘッドがありません。
静的に定義された条件変数をデフォルト属性に初期化するには、マクロ PTHREAD_COND_INITIALIZER を使用します。PTHREAD_COND_INITIALIZER マクロは、NULL 属性を指定して pthread_cond_init() を動的に割り当てた場合と同じ効果を適用します。エラーチェックは行われません。
複数のスレッドで同じ条件変数を同時に初期化または再初期化しないでください。条件変数を再初期化または削除する場合、アプリケーションでその条件変数が現在使用されていないことを確認しなければなりません。
pthread_cond_init() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を返します。
EINVAL
説明:cattr で指定された値が無効です。
EBUSY
説明:その条件変数は現在使用されています。
EAGAIN
説明:必要なリソースが利用できません。
ENOMEM
説明:メモリー不足のため条件変数を初期化できません。
mp が指す相互排他ロックを不可分操作により解放し、 cv が指す条件変数で呼び出しスレッドをブロックするには、pthread_cond_wait(3C) を使用します。
int pthread_cond_wait(pthread_cond_t *restrict cv,pthread_mutex_t *restrict mutex);
#include <pthread.h> pthread_cond_t cv; pthread_mutex_t mp; int ret; /* wait on condition variable */ 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 を再度獲得して pthread_cond_wait() から戻る前に、条件が変更される可能性があります。また、待機スレッドが誤って呼び起こされる可能性があります。条件チェックを while() ループに入れ、そこで pthread_cond_wait() を呼び出すようにすることをお勧めします。
pthread_mutex_lock(); while(condition_is_false) pthread_cond_wait(); pthread_mutex_unlock();
スケジューリングポリシーは、ブロックされたスレッドがどのように呼び起こされるかを決定します。デフォルトスケジューリングポリシー SCHED_OTHER は、スレッドが呼び起こされる順序を指定していません。SCHED_FIFO および SCHED_RR リアルタイムスケジューリングポリシーの下では、スレッドは優先順位に従って呼び起こされます。
pthread_cond_wait() は取り消しポイントです。保留状態になっている取り消しがあって、呼び出しスレッドが取り消しを有効 (使用可能) にしている場合、そのスレッドは終了し、ロックしている間にクリーンアップハンドラの実行を開始します。
pthread_cond_wait() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
EINVAL
説明:cv または mp で指定された値が無効です。
cv が指す条件変数でブロックされている 1 つのスレッドのブロックを解除するには、pthread_cond_signal(3C) を使用します。
int pthread_cond_signal(pthread_cond_t *cv);
#include <pthread.h> pthread_cond_t cv; int ret; /* one condition variable is signaled */ ret = pthread_cond_signal(&cv);
関連付けられた条件は、シグナルを送ろうとしている条件変数と同じ mutex ロックを獲得した状態で変更します。そうしないと、これらの条件変数が評価されてから pthread_cond_wait() でブロック状態に入るまでの間に条件変数が変更される可能性があり、その場合 pthread_cond_wait は永久に待ち続けることになります。
スケジューリングポリシーは、ブロックされたスレッドがどのように呼び起こされるかを決定します。デフォルトスケジューリングポリシー SCHED_OTHER は、スレッドが呼び起こされる順序を指定していません。SCHED_FIFO および SCHED_RR リアルタイムスケジューリングポリシーの下では、スレッドは優先順位に従って呼び起こされます。
条件変数でブロックされるスレッドがない場合、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_signal() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
EINVAL
説明:cv が指すアドレスが正しくありません。
例 4–8 は、pthread_cond_wait() と pthread_cond_signal() の使用例です。
pthread_cond_timedwait(3C)は、abstime で指定された時刻を過ぎるとブロック状態を解除する点を除いて、pthread_cond_wait() と同じ動作をします。()
int pthread_cond_timedwait(pthread_cond_t *restrict cv, pthread_mutex_t *restrict mp, const struct timespec *restrict abstime);
#include <pthread.h> #include <time.h> pthread_cond_t cv; pthread_mutex_t mp; timestruct_t abstime; int ret; /* wait on condition variable */ ret = pthread_cond_timedwait(&cv, & mp, &abstime);
pthread_cond_timedwait() は、たとえエラーを返す場合でも、常に呼び出しスレッドが mutex ロックし、所有した状態で終了します。()
pthread_cond_timedwait() のブロック状態が解除されるのは、条件変数にシグナルが送られてきたときか、一番最後の引数で指定した時刻を過ぎたときです。
pthread_cond_timedwait() は取り消しポイントでもあります。
pthread_timestruc_t to; pthread_mutex_t m; pthread_cond_t c; ... pthread_mutex_lock(&m); clock_gettime(CLOCK_REALTIME, &to); to.tv_sec += TIMEOUT; while (cond == FALSE) { err = pthread_cond_timedwait(&c, &m, &to); if (err == ETIMEDOUT) { /* timeout, do something */ break; } } pthread_mutex_unlock(&m);
pthread_cond_timedwait() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
EINVAL
説明:cv、mp、または abstime が不当なアドレスを指しています。
EINVAL
説明:同じ条件変数への並行した pthread_cond_timedwait() 操作に対して異なる mutex が指定されました。
ETIMEDOUT
説明:abstime で指定された時刻を過ぎています。
EPERM
説明:呼び出し時に、mutex が現在のスレッドによって所有されていませんでした。
時間切れの指定は時刻で行うため、時間切れ時刻を再計算する必要がなく、効率的に条件を再評価できます。例 4–9 を参照してください。
pthread_cond_reltimedwait_np(3C) の使用方法は、1 点を除いて、pthread_cond_timedwait() の使用方法と同じです。 pthread_cond_reltimedwait_np() の場合は、最後の引数として、未来の絶対日時ではなく相対時間間隔を指定します。
int pthread_cond_reltimedwait_np(pthread_cond_t *cv, pthread_mutex_t *mp, const struct timespec *reltime);
#include <pthread.h> #include <time.h> pthread_cond_t cv; pthread_mutex_t mp; timestruct_t reltime; int ret; /* wait on condition variable */ ret = pthread_cond_reltimedwait_np(&cv, &mp, &reltime);
pthread_cond_reltimedwait_np() は、たとえエラーを返す場合でも、常に呼び出しスレッドが mutex ロックし、所有した状態で終了します。()pthread_cond_reltimedwait_np() 関数は、条件のシグナルを受け取るか、最後の引数に指定されている時間間隔が経過するまで、ブロックします。
pthread_cond_reltimedwait_np() は、取り消しポイントでもあります。
pthread_cond_reltimedwait_np() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
EINVAL
説明:reltime で指定された値が無効です。
ETIMEDOUT
説明:reltime に指定されている時間間隔が経過しました。
pthread_cond_wait() で指定された cv が指す条件変数でブロックされている、すべてのスレッドのブロックを解除するには、pthread_cond_broadcast(3C) を使用します。
int pthread_cond_broadcast(pthread_cond_t *cv);
#include <pthread.h> pthread_cond_t cv; int ret; /* all condition variables are signaled */ ret = pthread_cond_broadcast(&cv);
スレッドがブロックされていない条件変数に対して pthread_cond_broadcast() を実行しても無視されます。
pthread_cond_broadcast() は、条件変数でブロックされていたすべてのスレッドにもう一度相互排他ロックを争奪させるので、慎重に使用してください。()たとえば、pthread_cond_broadcast() を使用して、可変量のリソースを、解放時にスレッド間で争奪させることができます。例 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() の呼び出しのどちらを先に行なってもかまいません。
関連付けられた条件は、シグナルを送ろうとしている条件変数と同じ mutex ロックを獲得した状態で変更します。そうしないと、これらの条件変数が評価されてから pthread_cond_wait() でブロック状態に入るまでの間に条件変数が変更される可能性があり、その場合 pthread_cond_wait は永久に待ち続けることになります。
pthread_cond_broadcast() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
EINVAL
説明:cv が指すアドレスが正しくありません。
cv が指す条件変数に関連付けられた状態を削除するには、pthread_cond_destroy(3C) を使用します。
int pthread_cond_destroy(pthread_cond_t *cv);
#include <pthread.h> pthread_cond_t cv; int ret; /* Condition variable is destroyed */ ret = pthread_cond_destroy(&cv);
条件変数の記憶領域は解放されません。
pthread_cond_destroy() は、正常終了時に 0 を返します。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
EINVAL
説明:cv で指定された値が無効です。
pthread_cond_signal() または pthread_cond_broadcast() を呼び出すとき、スレッドが条件変数に関連する相互排他ロックを保持していないと「呼び起こし忘れ」(lost wake-up) という問題が生じることがあります。
「呼び起こし忘れ」は次のすべての条件が揃った場合に発生します。
スレッドが pthread_cond_signal() または pthread_cond_broadcast() を呼び出す
待機しているスレッドが存在しない
シグナルは無効になり、失われます
この問題は、条件に関連付けられた相互排他ロックを保持しないでテスト対象の条件が変更された場合に限り発生します。関連付けられた mutex を保持した状態でテスト対象の条件を変更した場合には、pthread_cond_signal() と pthread_cond_broadcast() を呼び出すときにそれらがその mutex を保持しているかどうかは影響しません。
「生産者 / 消費者」問題は、並行プログラミングに関する問題の中でも一般によく知られているものの 1 つです。この問題は次のように定式化されます。サイズが有限の 1 個のバッファーと 2 種類のスレッドが存在します。一方のスレッドを生産者、もう一方のスレッドを消費者と呼びます。
生産者は、バッファーに空きができるまでデータを入れることができません。消費者は、生産者がバッファーに何か書き込むまで、このバッファーからデータを取り出すことができません。
特定の条件のシグナルを待つスレッドの待ち行列を条件変数で表すことにします。
例 4–11には、こうした待ち行列を 2 つ使用しています。1 つは less で、バッファー内の未使用スロットを待つ生産者のための待ち行列です。もう 1 つは more で、情報が格納されたバッファースロットを待つ消費者のための待ち行列です。また、バッファーが同時に複数のスレッドによってアクセスされないようにするために、相互排他ロック (mutex ロック) も使用しています。
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 () を呼び出します。pthread_cond_wait() は、「バッファー内に空きがある」を表す条件 less にシグナルが送られてくるのを待つスレッドの待ち行列に入ります。
同時に、pthread_cond_wait() の呼び出しによって、スレッドは mutex のロックを解除します。生産者スレッドは、条件が真になって消費者スレッドがシグナルを送ってくれるのを待ちます。例 4–12 を参照してください。条件にシグナルが送られてくると、less を待っている一番目のスレッドが呼び起こされます。しかし、そのスレッドは pthread_cond_wait() が戻る前に、mutex ロックを再び獲得する必要があります。
これにより、バッファーデータ構造への相互排他アクセスが保証されます。その後、生産者スレッドは、バッファーに本当に空きがあるか確認する必要があります。空きがあれば、最初の未使用スロットにデータを入れます。
このとき、バッファーにデータが入れられるのを消費者スレッドが待っている可能性があります。そのスレッドは、条件変数 more で待ち状態となっています。生産者スレッドはバッファーにデータを入れると、pthread_cond_signal() を呼び出して、待ち状態の最初の消費者を呼び起こします待ち状態の消費者がいないときは、この呼び出しは無視されます。
最後に、生産者スレッドは mutex ロックを解除して、ほかのスレッドがバッファーデータ構造を操作できるようにします。
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++; /* now: either b->occupied < BSIZE and b->nextin is the index of the next empty slot in the buffer, or b->occupied == BSIZE and b->nextin is the index of the next (occupied) slot that will be emptied by a consumer (such as b->nextin == b->nextout) */ pthread_cond_signal(&b->more); pthread_mutex_unlock(&b->mutex); }
assert() 文の用法に注意してください。NDEBUG を定義してコードをコンパイルした場合を除き、assert() はその引数が真 (0 以外) として評価された場合は何も行いません。その引数が偽 (0) として評価された場合、プログラムは終了します。こうしたアサーションはマルチスレッドプログラムで特に役立ちます。assert() は、アサーションが失敗すると実行時に発生した問題をただちに指摘します。assert() の効果はほかにもあり、役立つコメントを提供します。
/* 現在の状態: ... で始まるコメント部分も、assert で表現した方がよいかもしれません。しかし、論理式で表現するには複雑すぎるので、ここでは文章で表現しています。
assert の部分もコメント部分も、不変式になっています。これらの不変式は、プログラムの実行により偽の値に変更されない論理式ですが、次のような例外があります。この例外は、あるスレッドが不変式中のプログラム変数を変更している瞬間に起こります。もちろん、assert の論理式は、どのスレッドがいつ実行した場合でも常に真であるべきです。
不変式は非常に重要な手法です。プログラムテキストとして明示的に表現しなくても、プログラムを分析するときは不変式に置き換えて問題を考えることが大切です。
上記の生産者コード内のコメントで表現された不変式は、スレッドがそのコメントを含むコード部分を実行中には常に真となります。しかし、それを mutex_unlock() のすぐ後ろに移動すると、必ずしも常に真とはなりません。assert() のすぐ後ろに移動した場合は、真となります。
このため、この不変式は常に真となる特性を持っていますが、次のような例外があります。この例外は、生産者または消費者がバッファーの状態を変更しているときに起こります。スレッドは mutex の保護下でバッファーを操作しているとき、この不変式の値を一時的に偽にしてもかまいません。しかし、処理が完了したら不変式の値を再び真に戻さなければなりません。
例 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--; /* now: either b->occupied > 0 and b->nextout is the index of the next occupied slot in the buffer, or b->occupied == 0 and b->nextout is the index of the next (empty) slot that will be filled by a producer (such as b->nextout == b->nextin) */ pthread_cond_signal(&b->less); pthread_mutex_unlock(&b->mutex); return(item); }