この章では、スレッドで使用できる同期の手法と同期上の問題について説明します。
同期オブジェクトは、データと同じようにしてアクセスされるメモリー内の変数です。異なるプロセス内のスレッドは、通常はお互いに参照できませんが、スレッドが制御する共有メモリー内に格納されている同期オブジェクトを使用することにより、相互に同期をとることができます。
同期オブジェクトをファイルに置くこともできます。そうすれば、同期オブジェクトを作成したプロセスの消滅後も同期変数を有効にできます。
次の同期オブジェクトがあります。
相互排他ロック (mutex ロック)
条件変数
セマフォ
以下のような状況で、同期は効果を発揮します。
同期が、共有データの整合性を保証する唯一の手段である場合。
異なるプロセス内のスレッド間で同じ同期オブジェクトを共同で使用する場合。同期オブジェクトを初期化するのは、連携するそれらのプロセスの中の 1 つのプロセスに限るべきです。同期オブジェクトを初期化し直すと、そのロック状態が解除されることになるからです。
同期によって可変データの安全性を保証できる場合。
プロセスがファイルをマッピングし、自分のスレッドにレコード形式のロックを獲得させることができる場合。ロックがいったん獲得されると、そのファイルをマッピングしているプロセス内のスレッドのうち、ロックを保持しているスレッド以外がそのロックを獲得しようとすると、そのロックが解放されるまでブロックされます。
整数のような単一の基本的な変数をアクセスするときでも同期が効果を持つことがあります。整数がバスのデータ幅にそろっていない、または整数がバスのデータ幅より大きいマシンでは、1 回のメモリーロードに複数のメモリーサイクルが必要な可能性があるからです。こうした状況は SPARC(TM) 版アーキテクチャのマシンでは生じませんが、プログラムの移植性を考慮すると、この問題は無視できません。
32 ビットアーキテクチャでは、long
long
型は原子 [原子操作は、それ以上小さい操作に分割できません。] としての処理対象ではなく、2 つの 32 ビット値として読み書きされます。int
型、char
型、float
型、およびポインタは、SPARC 版マシンと IA マシンでは原子的です。
相互排他ロック (mutex ロック) は、スレッドの実行を直列化したいときに使用します。相互排他ロックでスレッド間の同期をとるときは、通常はコードの危険領域が複数のスレッドによって同時に実行されないようにするという方法が用いられます。単一のスレッドのコードを保護する目的で相互排他ロックを使用することもできます。
デフォルトの mutex 属性を変更するには、属性オブジェクトを宣言して初期化します。多くの場合、アプリケーションの先頭部分の一箇所で設定しますので、mutex 属性は、すばやく見つけて簡単に変更できます。表 4-1 に、この節で説明する mutex 属性操作関数を示します。
表 4-1 mutex 属性ルーチン
操作 |
参照先 |
---|---|
mutex 属性オブジェクトの初期化 | |
mutex 属性オブジェクトの削除 | |
mutex の適用範囲設定 | |
mutex のスコープの値の取得 | |
mutex の型属性の設定 | |
mutex の型属性の取得 | |
mutex 属性のプロトコルの設定 | |
mutex 属性のプロトコルの取得 | |
mutex 属性の優先順位上限の設定 | |
mutex 属性の優先順位上限の取得 | |
mutex の優先順位上限の設定 | |
mutex の 優先順位上限の取得 | |
mutex の堅牢度属性の設定 | |
mutex の堅牢度属性の取得 |
mutex のスコープ定義について、Solaris のスレッドと POSIX のスレッドとの相違点を表 4-2 に示します。
表 4-2 mutex の適用範囲の比較
Solaris |
POSIX |
定義 |
---|---|---|
USYNC_PROCESS |
PTHREAD_PROCESS_SHARED |
このプロセスと他のプロセスのスレッドの間で同期をとるために使用する |
USYNC_PROCESS_ROBUST |
POSIX に相当する定義なし |
異なるプロセスのスレッド間で安定的に同期をとるために使用する |
USYNC_THREAD |
PTHREAD_PROCESS_PRIVATE |
このプロセスのスレッドの間でだけ同期をとるために使用する |
pthread_mutexattr_init(3T) は、このオブジェクトに関連付けられた属性をデフォルト値に初期化します。各属性オブジェクトのための記憶領域は、実行時にスレッドによって割り当てられます。
この関数が呼び出されたときの 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 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を返します。
pthread_mutexattr_destroy(3T) は、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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を返します。
pthread_mutexattr_setpshared(3T) は、mutex 変数の適用範囲を設定します。
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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を返します。
pthread_mutexattr_getpshared(3T) は、pthread_mutexattr_setpshared() によって定義された、mutex 変数の適用範囲を返します。
プロトタイプ: 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); |
属性オブジェクト mattr の pshared の現在値を取得します。これは PTHREAD_PROCESS_SHARED と PTHREAD_PROCESS_PRIVATE のどちらかです。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を返します。
#include <pthread.h> int pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type); |
pthread_mutexattr_settype(3T) は、 mutex の 型 (type) 属性を設定します。型 属性のデフォルト値は PTHREAD_MUTEX_DEFAULT です。
型 (type) 引数は mutex の型を指定します。有効な mutex 型を以下に示します。
PTHREAD_MUTEX_NORMAL
この型の mutex はデッドロックを検出しません。スレッドが、この mutex をロック解除しないでもう一度ロックしようとすると、スレッドはデッドロックします。別のスレッドによってロックされた mutex をロック解除しようとした場合、引き起こされる動作は未定義です。また、ロック解除された mutex をロック解除しようとした場合、引き起こされる動作は不定です。
PTHREAD_MUTEX_ERRORCHECK
この型の mutex はエラーチェックを行います。スレッドがこの mutex をロック解除しないでもう一度ロックしようとすると、エラーを返します。別のスレッドがロックした mutex をロック解除しようとすると、エラーを返します。また、ロック解除された mutex をロック解除しようとするとエラーを返します。
PTHREAD_MUTEX_RECURSIVE
スレッドがこの mutex をロック解除しないでもう一度ロックしようとすると、正常にロックできます。PTHREAD_MUTEX_NORMAL 型の mutex ではロックを繰り返すとデッドロックが発生しますが、この型の mutex では発生しません。複数回ロックされた mutex を別のスレッドが獲得するときには、その前に同じ回数ロック解除する必要があります。あるスレッドがロックした mutex を別のスレッドがロック解除しようとすると、エラーが返されます。ロック解除されている mutex をスレッドがロック解除しようとすると、エラーが返されます。 mutex の型は、プロセス共有属性が PTHREAD_PROCESS_PRIVATE の mutex に対してだけサポートされます。
PTHREAD_MUTEX_DEFAULT
このタイプの mutex を繰り返しロックしようとした場合、引き起こされる動作は未定義です。この型の mutex を、ロックしていないスレッドがロック解除しようとした場合、引き起こされる動作は未定義です。この型の、ロックされていない mutex をロック解除しようとした場合、引き起こされる動作は未定義です。この型の mutex は、他の mutex 型に割り当てることができます。Solaris スレッドでは、PTHREAD_PROCESS_DEFAULT は PTHREAD_PROCESS_NORMAL に割り当てられます。
pthread_mutexattr_settype 関数は、正常に終了すると 0 を返します。それ以外の場合は、エラーを示す値を返します。
EINVAL
type の値が無効です。
EINVAL
attr で指定された値が無効です。
#include <pthread.h> int pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type); |
pthread_mutexattr_gettype(3T) は、pthread_mutexattr_settype() によって設定された、 mutex の 型 (type) 属性を取得します。型属性のデフォルト値は PTHREAD_MUTEX_DEFAULT です。
型 (type) 引数は mutex の型を指定します。有効な mutex 型を以下に示します。
PTHREAD_MUTEX_NORMAL
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_DEFAULT
各型の説明については、「pthread_mutexattr_settype(3T)」を参照してください。
pthread_mutexattr_setprotocol(3T) は、mutex 属性オブジェクトのプロトコル属性を設定します。
#include <pthread.h> int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol); |
attr は、先の pthread_mutexattr_init() の呼び出しによって作成された mutex 属性オブジェクトを指します。
protocol には、mutex 属性オブジェクトに適用されるプロトコルを指定します。
pthread.h に定義可能な protocol の値は、PTHREAD_PRIO_NONE、PTHREAD_PRIO_INHERIT、または PTHREAD_PRIO_PROTECT です。
PTHREAD_PRIO_NONE
PTHREAD_PRIO_INHERIT
スレッド (thrd1 など) が所有する 1 つまたは複数の mutex が、より優先順位の高いスレッドによってブロックされている場合、これらの mutex が PTHREAD_PRIO_INHERIT で初期化されていると、このプロトコル値はスレッド (thrd1) の優先順位とスケジューリングに影響します。thrd1 は、より高い優先順位または thrd1 が所有する mutex を待っているスレッドの最高優先順位で実行されます。
thrd1 が別のスレッド thrd3 が所有する mutex をブロックしている場合、同様の優先順位継承効果が thrd3 に対して再帰的に伝播されます。
PTHREAD_PRIO_INHERIT を使用して、優先順位が逆転しないようにしてください。優先順位の低いスレッドが、そのスレッドより優先順位の高いスレッドが必要としているロックを保持していると、優先順位が逆転します。優先順位の高いスレッドは、優先順位の低いスレッドがロックを解除するまで実行を続行できないため、各スレッドは本来の優先順位が逆転しているかのように扱われます。
シンボル _POSIX_THREAD_PRIO_INHERIT が定義されている場合、プロトコル属性値 PTHREAD_PRIO_INHERIT で初期化された mutex では、その mutex の所有者が終了すると Solaris オペレーティング環境で次の動作が発生します。
所有者終了時の動作は、pthread_mutexattr_setrobust_np() の robustness 引数の値によって異なります。
mutex のロックが解除されます。
次の所有者がその mutex を獲得し、エラーコード EOWNERDEAD が返されます。
mutex の次の所有者は、mutex によって保護されている状態を整合させるよう試行する必要があります。これは、前の所有者が終了したときに状態が不整合のままになっている可能性があるためです。所有者が状態を整合させることに成功すると、その mutex に対して pthread_mutex_init() を呼び出して、mutex をロック解除します。
pthread_mutex_init() が前の初期化で呼び出されたが、まだ mutex を削除していない場合、mutex は初期化し直されません。
所有者が状態を整合させることができない場合は、pthread_mutex_init() は呼び出さず、mutex をロック解除します。この場合には、すべての待機者が呼び起こされ、それ以降の pthread_mutex_lock() へのすべての呼び出しは mutex の獲得に失敗し、エラーコード ENOTRECOVERABLE が返されます。この時点で、pthread_mutex_destroy() を呼び出して mutex を削除し、pthread_mutex_init() を呼び出して初期化し直すことによって、mutex の状態を整合させることができます。
EOWNERDEAD を持つロックを獲得したスレッドが終了すると、次の所有者がエラーコード EOWNERDEAD を持つロックを獲得します。
PTHREAD_PRIO_PROTECT
あるスレッドが、PTHREAD_PRIO_PROTECT で初期化された 1 つまたは複数の mutex を所有する場合に、このプロトコル値は、スレッド (thrd2 など) の優先順位とスケジューリングに影響します。thrd2 は、より高い優先順位または自分が所有しているすべての mutex の中で最も高い優先順位で実行します。thrd2 が所有するいずれかの mutex でブロックされているより優先度の高いスレッドは、thrd2 のスケジューリングには影響を与えません。
スレッドが PTHREAD_PRIO_INHERIT または PTHREAD_PRIO_PROTECT で初期化された mutex を所有しており、sched_setparam() の呼び出しなどによってそのスレッドの元の優先順位が変更されている場合は、スケジューラは新しい優先順位のスケジューリングキューの末尾にそのスレッドを移動しません。同様に、PTHREAD_PRIO_INHERIT または PTHREAD_PRIO_PROTECT で初期化された mutex をスレッドがロック解除して、そのスレッドの元の優先順位が変更されている場合は、スケジューラは新しい優先順位のスケジューリングキューの末尾にそのスレッドを移動しません。
PTHREAD_PRIO_INHERIT で初期化された mutex と PTHREAD_PRIO_PROTECT で初期化された mutex を複数同時に所有しているスレッドは、これらのプロトコルのいずれかで獲得された最高の優先順位で実行します。
pthread_mutexattr_setprotocol() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次のどちらかの条件が検出されると、pthread_mutexattr_setprotocol() は失敗し、対応する値を返します。
_POSIX_THREAD_PRIO_INHERIT と_POSIX_THREAD_PRIO_PROTECT のどちらのオプションも定義されておらず、この実装はこの関数をサポートしていません。
protocol で指定された値はサポートされていない値です。
次のどちらかの条件が検出されると、pthread_mutexattr_setprotocol() は失敗し、対応する値を返します。
pthread_mutexattr_getprotocol(3T) は、mutex 属性オブジェクトのプロトコル属性を取得します。
#include <pthread.h> int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *attr, int *protocol); |
attr は、先の pthread_mutexattr_init() の呼び出しによって作成された mutex 属性オブジェクトを指します。
protocol には、プロトコル属性が入ります。値は、PTHREAD_PRIO_NONE、PTHREAD_PRIO_INHERIT、または PTHREAD_PRIO_PROTECT です。
pthread_mutexattr_getprotocol() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次の条件が検出されると、pthread_mutexattr_getprotocol() は失敗し、対応する値を返します。
_POSIX_THREAD_PRIO_INHERIT と _POSIX_THREAD_PRIO_PROTECT のどちらのオプションも定義されておらず、この実装はこの関数をサポートしていません。
次のどちらかの条件が検出されると、pthread_mutexattr_getprotocol() は失敗し、条件に対応する値を返します。
pthread_mutexattr_setprioceiling(3T) は、mutex 属性オブジェクトの優先順位上限属性を設定します。
#include <pthread.h> int pthread_mutexattr_setprioceiling(pthread_mutexattr_t *attr, int prioceiling, int *oldceiling); |
attr は、先の pthread_mutexattr_init() の呼び出しによって作成された mutex 属性オブジェクトを指します。
attr mutex 属性オブジェクトに優先順位上限属性が含まれるのは、シンボル _POSIX_THREAD_PRIO_PROTECT が定義されている場合だけです。
prioceiling には、初期化された mutex の優先順位上限を指定します。優先順位上限は、mutex によって保護されている重要領域が実行される最小の優先レベルを定義します。prioceiling は、SCHED_FIFO によって定義される優先順位の最大範囲内にあります。優先順位が逆転しないように、特定の mutex をロックするすべてのスレッドの中で最も高い優先順位と同じかまたはそれを上回る優先順位を prioceiling として設定します。
oldceiling には古い優先順位上限の値が入ります。
pthread_mutexattr_setprioceiling() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次のいずれかの条件が検出されると、pthread_mutexattr_setprioceiling() は失敗し、対応する値を返します。
次のどちらかの条件が検出されると、pthread_mutexattr_setprioceiling() は失敗し、対応する値を返します。
pthread_mutexattr_getprioceiling(3T) は、mutex 属性オブジェクトの優先順位上限属性を取得します。
#include <pthread.h> int pthread_mutexattr_getprioceiling(const pthread_mutexattr_t *attr, int *prioceiling); |
attr は、先の pthread_mutexattr_init() の呼び出しによって作成された属性オブジェクトを指します。
attr mutex 属性オブジェクトに優先順位上限属性が含まれるのは、シンボル _POSIX_THREAD_PRIO_PROTECT が定義されている場合だけです。
pthread_mutexattr_getprioceiling() は、初期化された mutex の優先順位上限、mutex を prioceiling で返します。この上限は、mutex によって保護されている重要領域が実行される最小の優先レベルを定義します。prioceiling は、SCHED_FIFO によって定義される優先順位の最大範囲内にあります。優先順位が逆転しないように、特定の mutex をロックするすべてのスレッドの中で最も高い優先順位と同じかまたはそれを上回る優先順位を prioceiling として設定します。
pthread_mutexattr_getprioceiling() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次の条件が検出されると、pthread_mutexattr_getprioceiling() は失敗し、対応する値を返します。
次のどちらかの条件が検出されると、pthread_mutexattr_getprioceiling() は失敗し、対応する値を返します。
pthread_mutex_setprioceiling(3T) は、mutex の優先順位上限を設定します。
#include <pthread.h> int pthread_mutex_setprioceiling(pthread_mutexatt_t *mutex, int prioceiling, int *old_ceiling); |
pthread_mutex_setprioceiling() は mutex の優先順位上限、つまり prioceiling を変更します。pthread_mutex_setprioceiling() は、mutex のロックが解除されている場合 mutex をロックするか、または mutex を正常にロックできるようになるまでブロックして、mutex の優先順位上限を変更し、mutex を開放します。mutex をロックするプロセスでは、優先順位保護プロトコルを守る必要はありません。
mutex 属性オブジェクト、つまり mutex に優先順位上限が含まれるのは、シンボル _POSIX_THREAD_PRIO_PROTECT が定義されている場合だけです。
pthread_mutex_setprioceiling() が正常に終了すると、優先順位上限の以前の値が old_ceiling で返されます。pthread_mutex_setprioceiling() が失敗すると、mutex の優先順位上限は元のままになります。
pthread_mutex_setprioceiling() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次の条件が検出されると、pthread_mutexatt_setprioceiling() は失敗し、それに対応する値を返します。
次のいずれかの条件が検出されると、 pthread_mutex_setprioceiling() は失敗し、対応する値を返します。
prioceiling で要求された優先順位が範囲外です。
mutex で指定された値は現在の既存の mutex を参照していません。
この実装は mutex の優先順位上限プロトコルをサポートしていません。
呼び出し元はこの操作を行うための権限を持っていません。
pthread_mutex_getprioceiling(3T) は、mutex の優先順位上限を取得します。
#include <pthread.h> int pthread_mutex_getprioceiling(const pthread_mutexatt_t *mutex, int *prioceiling); |
pthread_mutex_getprioceiling() は、mutex の優先順位上限、つまり prioceiling を返します。
pthread_mutex_getprioceiling() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次の条件が検出されると、 pthread_mutexatt_getprioceiling() は失敗し、対応する値を返します。
次のいずれかの条件が検出されると、 pthread_mutex_getprioceiling() は失敗し、対応する値を返します。
mutex で指定された値は現在の既存の mutex を参照していません。
この実装は mutex の優先順位上限プロトコルをサポートしていません。
呼び出し元はこの操作を行うための権限を持っていません。
pthread_mutexattr_setrobust_np(3T) は、mutex 属性オブジェクトの堅牢度属性を設定します。
#include <pthread.h> int pthread_mutexattr_setrobust_np(pthread_mutexatt_t *attr, int *robustness); |
pthread_mutexattr_setrobust_np() が適用されるのは、シンボル _POSIX_THREAD_PRIO_INHERIT が定義されている場合だけです。
attr は、先の pthread_mutexattr_init() の呼び出しによって作成された mutex 属性オブジェクトを指します。
robustness は、mutex の所有者が終了した場合の動作を定義します。pthread.h に定義可能な robustness の値は、 PTHREAD_MUTEX_ROBUST_NP または PTHREAD_MUTEX_STALLED_NP です。デフォルト値は、PTHREAD_MUTEX_STALLED_NP です。
PTHREAD_MUTEX_ROBUST_NP
mutex の所有者が終了すると、それ以降の pthread_mutex_lock() へのすべての呼び出しは、指定しない方法で進行過程からブロックされます。
PTHREAD_MUTEX_STALLED_NP
mutex の所有者が終了すると、mutex はロック解除されます。この mutex の次の所有者が獲得し、エラーコード EOWNERDEAD が返されます。
作成するアプリケーションは、このタイプの mutex について、pthread_mutex_lock() から出力される戻りコードをチェックする必要があります。
この mutex の新しい所有者は、mutex によって保護されている状態を整合させる必要があります。これは、前の所有者が終了したときに状態が不整合のままになっている可能性があるためです。
新しい所有者が状態を整合できる場合は、その mutex に対して pthread_mutex_consistent_np() を呼び出して、mutex をロック解除します。
新しい所有者が状態を整合できない場合は、その mutex に対して pthread_mutex_consistent_np() を呼び出さずに、mutex をロック解除してください。
すべての待機者が呼び起こされ、それ以降の pthread_mutex_lock() へのすべての呼び出しは mutex の獲得に失敗し、エラーコード ENOTRECOVERABLE を返します。この時点で、pthread_mutex_destroy() を呼び出して mutex を削除し、pthread_mutex_int() を呼び出して初期化し直すことによって、mutex の状態を整合させることができます。
EOWNERDEAD を持つロックを獲得したスレッドが終了すると、次の所有者がリターンコード EOWNERDEAD を持つロックを獲得します。
pthread_mutexattr_setrobust_np() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次の条件のいずれかが検出されると、pthread_mutexattr_setrobust_np() は失敗し、対応する値を返します。
オプション _POSIX_THREAD_PRIO__INHERIT が定義されていないか、あるいはこの実装が pthread_mutexattr_setrobust_np() 関数をサポートしていません。
robustness で指定された値はサポートされていません。
次の条件が検出されると、pthread_mutexattr_setrobust_np() は失敗します。
pthread_mutexattr_getrobust_np(3T) は、mutex 属性オブジェクトの堅牢度属性を取得します。
#include <pthread.h> int pthread_mutexattr_getrobust_np(pthread_mutexatt_t *attr, int *robustness); |
pthread_mutexattr_getrobust_np() が適用されるのは、シンボル_POSIX_THREAD_PRIO_INHERIT が定義されている場合だけです。
attr は、先の pthread_mutexattr_init() の呼び出しによって作成された mutex 属性オブジェクトを指します。
robustness は、mutex 属性オブジェクトの堅牢度属性の値です。
pthread_mutexattr_getrobust_np() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。
次の条件のいずれかが検出されると、pthread_mutexattr_getrobust_np() は失敗し、対応する値を返します。
オプション _POSIX_THREAD_PRIO__INHERIT が定義されていないか、あるいはこの実装が pthread_mutexattr_getrobust_np() 関数をサポートしていません。
robustness で指定された値はサポートされていません。
次の条件が検出されると、pthread_mutexattr_getrobust_np() は失敗します。
表 4-3 に、この章で説明する mutex ロック操作関数を示します。
表 4-3 相互排他ロック操作ルーチン
操作 |
参照先 |
---|---|
mutex の初期化 | |
mutex のロック | |
mutex のロック解除 | |
ブロックしないで行う mutex のロック | |
mutex の削除 |
デフォルトスケジューリング方針 SCHED_OTHER は、スレッドによるロックの獲得順序を指定していません。複数のスレッドが mutex を待っているときの獲得の順序は不定です。競合するときは、スレッドを優先順位でブロック解除するというのがデフォルト動作です。
pthread_mutex_init(3T) は、mp が指す mutex をデフォルト値に初期化 (mattr が NULL の場合) するか、pthread_mutexattr_init() ですでに設定されている mutex 属性を指定するときに使用します。(Solaris スレッドについては、「mutex_init(3T)」を参照)。
プロトタイプ: 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 は、ロック解除状態になります。mutex は、プロセス間で共有されているメモリー内または個々のプロセス専用のメモリー内に置かれます。
mutex メモリーは、初期化する前にクリアしてゼロにする必要があります。
mattr を NULL にするのは、デフォルト mutex 属性オブジェクトのアドレスを渡すのと同じことですが、メモリーのオーバーヘッドがありません。
mutex を静的に定義する場合、マクロ PTHREAD_MUTEX_INITIALIZER により、デフォルト属性を持つように直接初期化できます。
mutex ロックは、他のスレッドが使用している可能性がある間は再初期化したり削除したりしてはいけません。どちらの動作も正しく行われなければプログラムで障害が発生します。mutex を再初期化または削除する場合、アプリケーションがその mutex を使用していないことが確実でなければなりません。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を戻します。
#include <pthread.h> int pthread_mutex_consistent_np(pthread_mutex_t *mutex); |
pthread_mutex_consistent_np() が適用されるのは、シンボル _POSIX_THREAD_PRIO_INHERIT が定義され、かつプロトコル属性値 PTHREAD_PRIO_INHERIT で初期化されている mutex に対してのみです。
mutex の所有者が終了すると、mutex が不整合になる可能性があります。
pthread_mutex_consistent_np が、mutex の所有者の終了後に mutex オブジェクト、mutex を整合させます。
不整合の mutex を獲得するには、pthread_mutex_lock() を呼び出します。戻り値 EOWNERDEAD は不整合な mutex であることを示します。
pthread_mutex_consistent_np() は、pthread_mutex_lock() への前の呼び出しによって獲得された mutex を保持している間に呼び出してください。
mutex によって保護されている重要領域が、終了した所有者によって不整合の状態のままになっている可能性があるので、mutex によって保護されている重要領域を整合させることができる場合にのみ mutex を整合させてください。
整合された mutex に対して pthread_mutex_lock()、pthread_mutex_unlock() および pthread_mutex_trylock() を呼び出すと、通常の方法で動作します。
不整合でない、あるいは保持されていない mutex に対する pthread_mutex_consistent_np() の動作は、定義されていません。
pthread_mutex_consistent_np() は、正常終了すると 0 を返します。それ以外の戻り値は、エラーが発生したことを示しています。次の条件が検出されると、関数は失敗し、対応する値を返します。
次の条件が検出されると、pthread_mutex_consistent_np() は失敗します。
オプション _POSIX_THREAD_PRIO_INHERIT が定義されていないか、あるいはこの実装が pthread_mutex_consistent_np() 関数をサポートしていません。
次の条件が検出されると、pthread_mutex_consistent_np() は失敗します。
プロトタイプ: int pthread_mutex_lock(pthread_mutex_t *mutex); #include <pthread.h> pthread_mutex_t mutex; int ret; ret = pthread_ mutex_lock(&mp); /* mutex を獲得する */ |
pthread_mutex_lock(3T) は、mutex が指す mutex をロックします。pthread_mutex_lock() が戻ると、呼び出しスレッドが mutex をロックした状態になっています。mutex が別のスレッドによってすでにロックされている (所有されている) 場合は、呼び出しスレッドは mutex が使用可能になるまでブロックされます (Solaris スレッドについては、「mutex_lock(3T)」を参照)。
mutex 型が PTHREAD_MUTEX_NORMAL の場合、デッドロックの検出は行われません。mutex をもう一度ロックしようとするとデッドロックが発生します。スレッドが、ロックされていない mutex やロック解除された mutex をロック解除しようとした場合、引き起こされる動作は未定義です。
mutex 型が PTHREAD_MUTEX_ERRORCHECK の場合は、エラーチェックが提供されます。すでにロックされた mutex をもう一度ロックしようとすると、エラーが返されます。ロックされていない mutex やロック解除された mutex をロック解除しようとすると、エラーが返されます。
mutex 型が PTHREAD_MUTEX_RECURSIVE の場合は、mutex はロックの回数を記録します。スレッドが最初に正常に mutex を獲得すると、ロック計数は 1 に設定されます。この mutex をスレッドがさらにロックするたびに、ロックカウントが 1 ずつ増えます。スレッドが mutex をロック解除するたびに、ロックカウントが 1 ずつ減ります。ロックカウントが 0 になると、その mutex を別のスレッドが獲得できるようになります。ロックされていない mutex やロック解除された mutex をロック解除しようとすると、エラーが返されます。
mutex 型が PTHREAD_MUTEX_DEFAULT の場合、繰り返し mutex をロックしようとすると、引き起こされる動作は未定義です。 mutex をロックしていないスレッドがロック解除しようとした場合、引き起こされる動作は未定義です。また、ロックされていない mutex をロック解除しようとした場合、引き起こされる動作は未定義です。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。
シンボル _POSIX_THREAD_PRIO_INHERIT が定義されていて、mutex がプロトコル属性値 PTHREAD_PRIO_INHERIT で初期化されており、pthread_mutexattr_setrobust_np() の robustness 引数が PTHREAD_MUTEX_ROBUST_NP である場合、この関数は失敗し、次の値を返します。
この mutex の前の所有者が mutex を保持している間に終了しました。現在この mutex は、呼び出し元によって所有されています。呼び出し元は、mutex によって保護された状態を整合させるよう試行する必要があります。
呼び出し元が状態を整合させることができた場合、その mutex に対して pthread_mutex_consistent_np() を呼び出して、mutex をロック解除します。これ以降の pthread_mutex_lock() の呼び出しは正常に動作します。
呼び出し元が状態を整合させることができない場合は、その mutex に対して pthread_mutex_init() は呼び出さず、mutex をロック解除します。これ以降の pthread_mutex_lock() のすべての呼び出しは mutex の獲得に失敗し、エラーコード ENOTRECOVERABLE を返します。
EOWNERDEAD を持つロックを獲得した所有者が終了すると、次の所有者が EOWNERDEAD を持つロックを獲得します。
獲得しようとしている mutex は、ロックの保持中に終了した前の所有者によって回復不能にされた状態を保護しています。mutex は獲得されませんでした。ロックが以前に EOWNERDEAD を指定されて獲得され、所有者が状態をクリーンアップできず、mutex の状態を整合させないで mutex をロック解除した場合に、この状況が発生します。
同時に保持される mutex の上限数を超えています。
pthread_mutex_unlock(3T) は、mutex が指す mutex のロックを解除します。(Solaris スレッドについては、「mutex_unlock(3T)」を参照)。
プロトタイプ: int pthread_mutex_unlock(pthread_mutex_t *mutex); #include <pthread.h> pthread_mutex_t mutex; int ret; ret = pthread_mutex_unlock(&mutex); /* mutex を解除する */ |
pthread_mutex_unlock() は、mutex が指す mutex オブジェクトを解放します。mutex を解放する方法は、mutex の 型属性に依存します。pthread_mutex_unlock() が呼び出されたときに、指定された mutex が指す mutex オブジェクトでブロックされているスレッドがあり、この呼び出しによって mutex が使用できるようになると、スケジューリング方針に基づいて mutex を獲得するスレッドが決定されます。PTHREAD_MUTEX_RECURSIVE のタイプの mutex の場合、mutex が使用可能になるのは、カウントが 0 になり、pthread_mutex_unlock() を呼び出したスレッドがこの mutex のロックを解除したときです。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
pthread_mutex_trylock(3T) は、mutex が指す mutex のロックを試みます。(Solaris スレッドについては、「mutex_trylock(3T)」を参照)。
プロトタイプ: int pthread_mutex_trylock(pthread_mutex_t *mutex); #include <pthread.h> pthread_mutex_t mutex; int ret; ret = pthread_mutex_trylock(&mutex); /* mutex のロックを試みる */ |
この関数はブロックしない点を除いて、pthread_mutex_lock() と同じ働きをします。mutex が参照している mutex オブジェクトが、現在のスレッドを含むいずれかのスレッドによってロックされている場合は、呼び出しはただちに返されます。mutex オブジェクトがロックされていなければ、呼び出しスレッドがロックを獲得します。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。
シンボル _POSIX_THREAD_PRIO_INHERIT が定義されていて、mutex がプロトコル属性値 PTHREAD_PRIO_INHERIT で初期化されており、pthread_mutexattr_setrobust_np() の robustness 引数が PTHREAD_MUTEX_ROBUST_NP である場合、この関数は失敗し、次の値を返します。
この mutex の前の所有者が mutex を保持している間に終了しました。現在この mutex は、呼び出し元によって所有されています。呼び出し元は、mutex によって保護された状態を整合させるよう試行する必要があります。
呼び出し元が状態を整合させることができた場合、その mutex に対して pthread_mutex_consistent_np() を呼び出して、mutex をロック解除します。これ以降の pthread_mutex_lock() の呼び出しは正常に動作します。
呼び出し元が状態を整合させることができない場合は、その mutex に対して pthread_mutex_init() は呼び出さず、mutex をロック解除します。これ以降の pthread_mutex_trylock() のすべての呼び出しは mutex の獲得に失敗し、エラーコード ENOTRECOVERABLE を返します。
EOWNERDEAD を持つロックを獲得した所有者が終了すると、次の所有者が EOWNERDEAD を持つロックを獲得します。
獲得しようとしている mutex は、ロックの保持中に終了した前の所有者によって回復不能にされた状態を保護しています。mutex は獲得されませんでした。ロックが以前に EOWNERDEAD を指定されて獲得され、所有者が状態をクリーンアップできず、mutex の状態を整合させないで mutex をロック解除した場合に、この状況が発生します。
同時に保持される mutex の上限数を超えています。
pthread_mutex_destroy(3T) は、mp が指す mutex に関連するすべての状態を削除します。(Solaris スレッドについては、「mutex_destroy(3T)」を参照)。
プロトタイプ: 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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
例 4-1 に、mutex ロックを示すコードの一部を示します。
#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 つの関数は、相互排他 (mutex) ロックをそれぞれ別の目的で使用しています。increment_count() 関数は、相互排他ロックによって共有変数の原子的操作による更新を保証しています。get_count() 関数は、相互排他ロックによって 64 ビット値の count が原子的に読み取られるようにしています。32 ビットアーキテクチャでは、long
long
は実際には 2 つの 32 ビット値として処理されます。
整数はほとんどのマシンで共通のワードサイズであるため、整数値の読み取りは原子操作です。
同時に 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 のロックを解除する必要があります。
スレッド 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); |
例 4-3 では、スレッド 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 つのロックを獲得する場合を説明します。この例では、デッドロックを防ぐために規定された順序でロックします。
typedef struct node1 { int value; struct node1 *link; pthread_mutex_t lock; } node1_t; node1_t ListHead; |
この例で使用する片方向リンクのリスト構造体は、各ノードに相互排他ロックを含んでいます。このリストから特定のノードを削除する場合は、最初に ListHead (これが削除されることはない) の位置からリストをたどって目的のノードを探します。
この検索を同時並行的に行われる削除から保護するために、各ノードをロックしてからノードの内容にアクセスしなければなりません。すべての検索が ListHead の位置から開始されるので、常にリストの順序でロックされます。このため、デッドロックは決して発生しません。
目的のノードが見つかった時は、この変更がそのノードと直前のノードの両方に影響を与えるため、両方をロックします。直前のノードのロックが常に最初に獲得されるので、ここでもデッドロックの心配はありません。例 4-5 は、片方向リンクリストから特定のノードを削除する C コードを示しています。
例 4-6 は、前述のリスト構造を修正して循環リストにしたものです。先頭のノードとして識別されるノードはありません。スレッドは適当な 1 つのノードに関連付けられると、そのノードと次のノードに対して操作を行います。この状況ではロック序列は適用できません。明らかに階層 (つまり、リンクをたどる順番) が循環的だからです。
typedef struct node2 { int value; struct node2 *link; pthread_mutex_t lock; } node2_t; |
例 4-7 では 2 つのノードをロックし、両方のノードに対してある操作を行なっている C コードを示します。
条件変数は、ある条件が真になるまでスレッドを原子的にブロックしたいときに使用します。必ず相互排他ロックとともに使用します。
条件変数を使うと、特定の条件が真になるまでスレッドを原子的にブロックできます。この条件判定は、相互排他ロックにより保護された状態で行います。
条件が偽のとき、スレッドは通常は条件変数でブロック状態に入り、相互排他ロックを原子的操作により解除して、条件が変更されるのを待ちます。別のスレッドが条件を変更すると、そのスレッドはそれに関連する条件変数にシグナルを送り、その条件変数でブロックしているスレッドを呼び起こします。呼び起こされたスレッドは再度相互排他ロックを獲得し、条件を再び評価します。
異なるプロセスに所属するスレッドの間で、条件変数を使って同期をとるためには、連携するそれらのプロセスの間で共有される書き込み可能なメモリーに、条件変数の領域を確保する必要があります。
スケジューリング方針は、ブロックされたスレッドがどのように呼び起こさるかを決定します。デフォルト SCHED_OTHER の場合、スレッドは優先順位に従って呼び起こされます。
条件変数の属性は、使用する前に設定して初期化しておかなければなりません。条件変数の属性を操作する関数を表 4-4 に示します。
表 4-4 条件変数の属性
操作 |
参照先 |
---|---|
条件変数の属性の初期化 | |
条件変数の属性の削除 | |
条件変数のスコープの設定 | |
条件変数のスコープの取得 |
条件変数のスコープ定義について、 Solaris スレッドと POSIX スレッドの相違点を表 4-5 に示します。
表 4-5 条件変数のスコープの比較
Solaris |
POSIX |
定義 |
---|---|---|
USYNC_PROCESS |
PTHREAD_PROCESS_SHARED |
このプロセスと他のプロセスのスレッドの間で同期をとるために使用する。 |
USYNC_THREAD |
PTHREAD_PROCESS_PRIVATE |
このプロセスのスレッドの間でだけ同期をとるために使用する。 |
pthread_condattr_init(3T) は、このオブジェクトに関連付けられた属性をデフォルト値に初期化します。各属性オブジェクトのための記憶領域は、実行時にスレッドシステムによって割り当てられます。この関数が呼び出されたときの pshared 属性のデフォルト値は PTHREAD_PROCESS_PRIVATE で、初期化された条件変数を 1 つのプロセスの中だけで使用できるという意味です。
プロトタイプ: int pthread_condattr_init(pthread_condattr_t *cattr); #include <pthread.h> pthread_condattr_t cattr; int ret; /* initialize an attribute to default value */ ret = pthread_condattr_init(&cattr); |
cattr は不透明なデータ型で、システムによって割り当てられた属性オブジェクトを格納します。cattr のスコープとして取りうる値は、PTHREAD_PROCESS_PRIVATE (デフォルト) と PTHREAD_PROCESS_SHARED です。
条件変数属性を再使用するには、pthread_condattr_destroy(3T) によって事前に削除しなければなりません。pthread_condattr_init() 呼び出しは、不透明なオブジェクトへのポインタを戻します。そのオブジェクトが削除されないと、結果的にメモリーリークを引き起こします。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を返します。
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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を返します。
pthread_condattr_setpshared(3T) は、プロセス専用 (プロセス内) とシステム共通 (プロセス間) のどちらかに条件変数のスコープを設定します。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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を返します。
pthread_condattr_getpshared(3T) は属性オブジェクト cattr の pshared の現在のスコープ値を取得します。これは PTHREAD_PROCESS_SHARED と PTHREAD_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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、対応する値を返します。
この節では条件変数の使用方法を説明します。表 4-6 にそのための関数を示します。
表 4-6 条件変数関数
操作 |
参照先 |
|
---|---|---|
条件変数の初期化 | ||
条件変数によるブロック | ||
特定のスレッドのブロック | ||
時刻指定のブロック | ||
全スレッドのブロック解除 | ||
条件変数の削除 |
pthread_cond_init(3T) は、cv が指す条件変数をデフォルト値 (cattr が NULL) に初期化します。また、pthread_condattr_init() ですでに設定してある条件変数の属性を指定することもできます。cattr を NULL にするのは、デフォルト条件変数属性オブジェクトのアドレスを渡すのと同じですが、メモリーのオーバーヘッドがありません。(Solaris スレッドについては、「cond_init(3T)」を参照)。
プロトタイプ: 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 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を返します。
cattr で指定された値が無効です。
その条件変数は現在使用されています。
必要なリソースが利用できません。
メモリー不足のため条件変数を初期化できません。
pthread_cond_wait(3T) は、mp が指す相互排他ロックを原子操作により解放し、cv が指す条件変数で呼び出しスレッドをブロックします。(Solaris スレッドについては、「cond_wait(3T)」を参照)。
プロトタイプ: 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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
pthread_cond_signal(3T) は、cv が指す条件変数でブロックされている 1 つのスレッドのブロックを解除します。(Solaris スレッドについては、「cond_signal(3T)」を参照)。
プロトタイプ: 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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
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); } |
プロトタイプ: 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(3T) は、abstime で指定した時刻を過ぎるとブロック状態を解除する点を除いて、pthread_cond_wait() と同じ動作をします。pthread_cond_timedwait() が戻るときは、たとえエラーを戻したときでも、常に mutex は呼び出しスレッドがロックして保持している状態です。(Solaris スレッドについては、「cond_timedwait(3T)」を参照)。
pthread_cond_timedwait() のブロック状態が解除されるのは、条件変数にシグナルが送られてきたときか、一番最後の引数で指定した時刻を過ぎたときです。
pthread_cond_timedwait() は、取り消しポイントでもあります。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
時間切れの指定は時刻で行うため、時間切れ時刻を再計算する必要がないので、効率的に条件を再評価できます (詳細は、例 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); |
プロトタイプ: 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(3T) は、cv (pthread_cond_wait() で指定された) が指す条件変数でブロックされている、すべてのスレッドのブロックを解除します。スレッドがブロックされていない条件変数に対して pthread_cond_broadcast() を実行しても無視されます。(Solaris スレッドについては、「cond_broadcast(3T)」を参照)。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
条件変数でブロックされていたすべてのスレッドが、もう一度相互排他ロックを争奪するようになるので慎重に使用してください。たとえば、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() の呼び出しはどちらを先に行なってもかまいません。
pthread_cond_broadcast() は、シグナルを送ろうとしている条件変数で使用されたものと同じ相互排他ロックを獲得した状態で呼び出してください。そうしないと、関連する条件変数が評価されてから pthread_cond_wait() でブロック状態に入るまでの間に条件変数にシグナルが送られる可能性があり、その場合 pthread_cond_wait() は永久に待ち続けることになります。
pthread_cond_destroy(3T) は、cv が指す条件変数を削除します。(Solaris スレッドについては、「cond_destroy(3T)」を参照)。
プロトタイプ: int pthread_cond_destroy(pthread_cond_t *cv); #include <pthread.h> pthread_cond_t cv; int ret; /* 条件変数を削除する */ ret = pthread_cond_destroy(&cv); |
条件変数の記憶領域は解放されません。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
pthread_cond_signal() または pthread_cond_broadcast() を呼び出すとき、スレッドが条件変数に関連する相互排他ロックを保持していないと「呼び起こし忘れ」(lost wake-up) という問題が生じることがあります。
次のすべての条件に該当する場合は、そのシグナルには効果がないので「呼び起こし忘れ」が発生します。
あるスレッドが pthread_cond_signal() または pthread_cond_broadcast() を呼び出す
待ち状態のスレッドがない
「生産者 / 消費者」問題は、並行プログラミングに関する問題の中でも一般によく知られているものの 1 つです。この問題は次のように定式化されます。サイズが有限の 1 個のバッファと 2 種類のスレッドが存在します。一方のスレッドを生産者、もう一方のスレッドを消費者と呼びます。生産者がバッファにデータを入れ、消費者がバッファからデータを取り出します。
生産者は、バッファに空きができるまでデータを入れることができません。消費者は、バッファが空の間はデータを取り出すことができません。
特定の条件のシグナルを待つスレッドの待ち行列を条件変数で表すことにします。
例 4-11 では、そうした待ち行列として less と more の 2 つを使用しています。less はバッファ内の未使用スロットを待つ生産者のための待ち行列で、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() を呼び出して、「バッファ内に空きがある」を表す条件 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++; /* 現在の状態: 「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 は、消費者の処理です。この処理の流れは生産者の場合と対称的です。
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 とともに条件変数を使用する場合は、プログラムのどの部分を保護するかが自然な形で明らかになりました。ところが、セマフォでは必ずしもそうはなりません。強力だからといって安易に使うとプログラムが不統一で理解しにくくなります。このため、「並行プログラミングにおける go to」と呼ばれています。
セマフォは、負の値をとらない整数のカウンタと考えることができます。通常は、リソースに対するアクセスの調整をはかる目的で、次のように使用されます。最初に、使用可能なリソースの数をセマフォに初期設定します。その後、スレッドはリソースが追加されるときにセマフォの値を原子的操作によって 1 増やし、リソースが削除されるときに原子的操作によって 1 減らします。
セマフォの値が 0 になった場合は、リソースがないことを意味します。この場合、セマフォの値を 1 減らそうとすると、スレッドはセマフォの値が 0 より大きくなるまでブロックされます。
表 4-7 セマフォに関するルーチン
操作 |
参照先 |
|
---|---|---|
セマフォの初期化 | ||
セマフォの加算 | ||
セマフォの値によるブロック | ||
セマフォの減算 | ||
セマフォの削除 |
セマフォは、その獲得と解放を同じスレッドで行う必要がないため、シグナルハンドラで行われているような非同期のイベント通知を実現できます。また、セマフォ自身が状態を持っているため、条件変数を使用する場合と違って相互排他ロックを獲得しなくても非同期で使用できます。ただし、セマフォは相互排他ロックほど効率的ではありません。
セマフォで複数のスレッドがブロックされているとき、それらのスレッドがどの順番でブロック解除されるかは、特に指定しなければ不定です。
セマフォは、使用する前に初期化されている必要がありますが、属性はありません。
プロトタイプ: 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(3R) は、sem が指すセマフォ変数を value の値に初期設定します。pshared の値が 0 なら、そのセマフォはプロセス間で共有できません。pshared の値が 0 以外なら、そのセマフォはプロセス間で共有できます。(Solaris スレッドについては、「sema_init(3T)」を参照)。
複数のスレッドから同じセマフォを初期化してはいけません。
また、一度初期化したセマフォは、他のスレッドで使用されている可能性があるので再初期化してはいけません。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件のいずれかが検出されると、この関数は失敗し、次の値を戻します。
value の値が SEM_VALUE_MAX を超えています。
そのセマフォを初期化するのに必要なリソースが使い果たされています。セマフォの制限 SEM_NSEMS_MAX に達しています。
そのセマフォを初期化するのに必要な特権をそのプロセスがもっていません。
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) のマニュアルページを参照してください。
プロトタイプ: int sem_post(sem_t *sem); #include <semaphore.h> sem_t sem; int ret; ret = sem_post(&sem); /* セマフォを加算する */ |
sem_post(3R) は、sem が指すセマフォの値を原子操作によって 1 増やします。そのセマフォでブロックされているスレッドがある場合は、そのスレッドのうちの 1 つのスレッドがブロック解除されます。(Solaris スレッドについては、「sema_post(3T)」を参照)。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
プロトタイプ: int sem_wait(sem_t *sem); #include <semaphore.h> sem_t sem; int ret; ret = sem_wait(&sem); /* セマフォの値の変化を待つ */ |
sem_wait(3R) は、sem が指すセマフォの値が 0 より大きくなるまでスレッドをブロックし、0 より大きくなったらセマフォの値を原子操作によって 1 減らします。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を返します。
プロトタイプ: int sem_trywait(sem_t *sem); #include <semaphore.h> sem_t sem; int ret; ret = sem_trywait(&sem); /* セマフォの値の変化を待つ */ |
sem_trywait(3R) は、sem が指すセマフォの値が 0 より大きい場合は原子操作によって 1 減らします。この関数はブロックしない点を除いて、sem_wait() と同じ働きをします。つまり、失敗した場合にはすぐに戻ります。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を返します。
sem が無効なアドレスを指しています。
この関数にシグナルが割り込みを行いました。
そのセマフォはすでにロックされているので、sem_trywait() でただちにロックできません。
プロトタイプ: int sem_destroy(sem_t *sem); #include <semaphore.h> sem_t sem; int ret; ret = sem_destroy(&sem); /* セマフォを削除する */ |
sem_destroy(3R) は、sem が指すセマフォを削除します。セマフォの記憶領域は解放されません。(Solaris スレッドについては、「sema_destroy(3T)」を参照)。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を返します。
例 4-14 のデータ構造は、条件変数による「生産者 / 消費者」問題のコード例 (例 4-11 参照) のデータ構造と似ています。2 つのセマフォでそれぞれ、バッファの使用済スロット数と未使用スロット数を表します。これらのセマフォは、未使用スロットができるまで生産者を待たせ、使用済スロットができるまで消費者を待たせます。
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 を使用すべきですが、セマフォの使用例を示すために特に使用しています。
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); } |
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); } |
読み取り / 書き込みロック によって、保護された共有リソースに対する並行する複数の読み取りと排他的な書き込みが可能になります。読み取り / 書き込みロックは単一の実体で、「読み取り」または「書き込み」モードでロック可能です。リソースを変更するには、スレッドはまず排他的な書き込みロックを獲得する必要があります。排他的に書き込みロックは、全ての読み取りロックが解放されるまで有効になりません。
データベースへのアクセスは読み取り / 書き込みロックに同期させることができます。読み取り操作によってレコードの情報が変更されることはないので、読み取り / 書き込みロックではデータベースのレコードを並行して読み取ることができます。データベースを更新する場合、書き込み操作は排他的な書き込みロックを獲得しなければなりません。
デフォルトの読み取り / 書き込みロック属性を変更するには、属性オブジェクトを宣言し、これを初期化しなければなりません。読み取り / 書き込みロック属性はアプリケーションのコードの開始位置にまとめて設定してある場合が多いので、その場所を素早く見つけて簡単に修正できます。ここで説明した読み取り / 書き込みロック属性を操作する関数を、次の表に示します。
読み書きロックに関する Solaris スレッドの実装については、「pthread に相当するものがある同期関数 - 読み取り / 書き込みロック」 を参照してください。
表 4-8 読み取り / 書き込みロック属性のルーチン
操作 |
参照先 |
|
---|---|---|
読み取り/書き込みロック属性の初期化 | ||
読み取り/書き込みロック属性の削除 | ||
読み取り/書き込みロック属性の設定 | ||
読み取り/書き込みロック属性の取得 |
#include <pthread.h> int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); |
pthread_rwlockattr_init(3T) は、読み取り / 書き込みロック属性オブジェクト attr の、実装によって定義されたすべての属性を、デフォルト値に初期化します。
初期化済みの読み取り / 書き込みロック属性オブジェクトを指定して pthread_rwlockattr_init を呼び出した場合、その結果は未定義です。読み取り / 書き込みロック属性オブジェクトを使って初期化された読み取り / 書き込みロックは、属性オブジェクトに影響を与えるどんな関数(削除を含む)の影響も受けないためです。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
#include <pthread.h> int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); |
pthread_rwlockattr_destroy(3T) は、読み取り / 書き込みロック属性オブジェクトを削除します。削除したオブジェクトを、pthread_rwlockattr_init() の呼び出しによって再び初期化する前に使った場合、その結果は未定義です。実装によっては、pthread_rwlockattr_destroy() は、attr が参照するオブジェクトに不正な値を設定する場合もあります。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
#include <pthread.h> int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); |
pthread_rwlockattr_setpshared(3T) は、プロセス共有の読み取り / 書き込みロック属性を設定します。
PTHREAD_PROCESS_SHARED
読み取り / 書き込みロックが割り当てられているメモリーにアクセスできるすべてのスレッドに、読み取り / 書き込みロックの操作を許可します。複数のプロセスによって共有されているメモリに置かれた読み取り / 書き込みロックに対しても有効です。
PTHREAD_PROCESS_PRIVATE
読み取り / 書き込みロックを操作できるのは、そのロックを初期化したスレッドと同じプロセス内で作成されたスレッドだけです。異なるプロセスのスレッドから読み取り / 書き込みロックを操作しようとした場合、その結果は未定義です。プロセス共有の属性のデフォルト値は、PTHREAD_PROCESS_PRIVATE です。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
#include <pthread.h> int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared); |
pthread_rwlockattr_getpshared(3T) は、プロセス共有の読み取り / 書き込みロック属性を取得します。
pthread_rwlockattr_getpshared() は、attr が参照する初期化済みの属性オブジェクトから、プロセス共有の属性の値を取得します。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
読み取り / 書き込みロックの属性を設定したあとに、読み取り / 書き込みロックそのものを初期化します。次の関数を使って、読み取り / 書き込みロックを初期化または削除したり、ロックまたはロック解除したり、ロックを試みたりできます。ここで説明した読み取り / 書き込みロック属性を操作する関数を、次の表に示します。
表 4-9 読み取り / 書き込みロック属性のルーチン
操作 |
参照先 |
|
---|---|---|
読み取り/書き込みロックの初期化 | ||
読み取り/書き込みロックの読み取りロック | ||
非ブロック読み取り/書き込みロックの読み取りロック | ||
読み取り/書き込みロックの書き込みロック | ||
非ブロック読み取り/書き込みロックの書き込みロック | ||
読み取り/書き込みロックの解除 | ||
読み取り/書き込みロックの削除 |
#include <pthread.h> int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; |
pthread_rwlock_init(3T) により、attr を参照される属性を使用して、rwlock が参照する読み取り / 書き込みロックを初期化します。attr が NULL の場合、デフォルトの読み取り / 書き込みロック属性が使われます。この場合の結果は、デフォルトの読み取り / 書き込みロック属性オブジェクトのアドレスを渡す場合と同じです。いったん初期化したロックは、繰り返して使用するために再び初期化する必要はありません。初期化が成功すると、読み取り / 書き込みロックは初期化され、ロックが解除された状態になります。初期化済みの読み取り / 書き込みロックを指定して、pthread_rwlock_init() を呼び出した場合、その結果は不定です。最初に初期化しないで読み取り / 書き込みロックを使用した場合も、その結果は不定です。Solaris スレッドについては、「rwlock_init(3T)」を参照してください。
デフォルトの読み取り / 書き込みロック属性を使用するのであれば、PTHREAD_RWLOCK_INITIALIZER というマクロを使用して、静的に割り当てられている読み取り / 書き込みロックを初期化できます。この場合の結果は、パラメータ attr に NULL を指定して pthread_rwlock_init() を呼び出し、動的に初期化したときと同じです。ただし、エラーチェックが実行されません。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
pthread_rwlock_init() が正常に終了しなかった場合、rwlock は初期化されず、rwlock の内容は未定義です。
#include <pthread.h> int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); |
pthread_rwlock_rdlock(3T) は、rwlock が参照する読み取り / 書き込みロックに読み取りロックを適用します。書き込みがロックを保持せず、読み取り / 書き込みロックでブロックされている書き込みもない場合は、呼び出しスレッドは読み取りロックを獲得します。書き込みがロックを保持せず、ロック待ちの書き込みがある場合は、呼び出しスレッドが読み取りロックを獲得するかどうかは不定です。書き込みが読み取り / 書き込みロックを保持している場合は、呼び出しスレッドは読み取りロックを獲得しません。読み取りロックが獲得されない場合、呼び出しスレッドは読み取りロックを獲得するまでブロックします。つまり、呼び出しスレッドは、pthread_rwlock_rdlock() から戻り値を取得しません。呼び出し時に、呼び出しスレッドが rwlock に書き込みロックを保持する場合、その結果は不定です。
書き込み側がいつまでもロックを獲得できない事態を避けるために、書き込みが読み取りに優先するように実装できます。たとえば、Solaris スレッドの実装では、書き込みが読み取りに優先します。「rw_rdlock(3T) 」 を参照してください。
スレッドは、rwlock に複数の並行的な読み取りロックを保持できます。つまり、pthread_rwlock_rdlock() の呼び出しが n 回成功します。この場合、スレッドは同数の読み取りロック解除を行わなければなりません。つまり、pthread_rwlock_unlock() を n 回呼び出さなければなりません。
pthread_rwlock_rdlock() が、初期化されていない読み取り / 書き込みロックに対して呼び出された場合、その結果は不定です。
読み取りのための読み取り / 書き込みロックを待っているスレッドにシグナルが送られた場合、スレッドはシグナルハンドラから戻ると、見かけ上割り込みがなかった場合と同様に、読み取りのための読み取り / 書き込みロック待ちを再開します。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
#include <pthread.h> int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); |
pthread_rwlock_tryrdlock(3T) は、pthread_rwlock_rdlock() と同様に読み取りロックを適用します。ただし、いずれかのスレッドが rwlock に書き込みロックを保持しているか、rwlock で書き込みスレッドがブロックされている場合、この関数は失敗します。Solaris スレッドについては、「rw_tryrdlock(3T)」 を参照してください。
rwlock が参照する読み取り / 書き込みロックオブジェクトに対する読み取りロックが獲得された場合、戻り値は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
書き込みが読み取り / 書き込みロックを保持しているか、読み取り / 書き込みロックで書き込みスレッドがブロックされているため、読み取りのための読み取り / 書き込みロックを獲得できません。
#include <pthread.h> int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); |
pthread_rwlock_wrlock(3T) は、rwlock が参照する読み取り / 書き込みロックに書き込みロックを適用します。 ほかのスレッド (読み取り側または書き込み側) が rwlock という読み取り / 書き込みロックを保持していない場合、呼び出しスレッドは書き込みロックを獲得します。これ以外の場合、スレッドは、ロックを獲得するまでブロックされます。つまり、pthread_rwlock_wrlock() の呼び出しから戻りません。呼び出し時に、呼び出しスレッドが読み取り / 書き込みロックを保持している場合 (読み取りロックと書き込みロックのどちらでも) の結果は不定です。
書き込み側がいつまでもロックを獲得できない事態を避けるために、書き込みが読み取りに優先するように実装することが許されています。たとえば、Solaris スレッドの実装では、書き込みが読み取りに優先します。「rw_wrlock(3T) 」を参照してください。
pthread_rwlock_wrlock() が、初期化されていない読み取り / 書き込みロックに対して呼び出された場合、その結果は不定です。
書き込みのための読み取り / 書き込みロックを待っているスレッドにシグナルが送られた場合、スレッドはシグナルハンドラから戻ると、見かけ上割り込みがなかった場合と同様に、書き込みのための読み取り / 書き込みロック待ちを再開します。
rwlock が参照する読み取りは / 書き込みロックオブジェクトの書き込みロックが獲得された場合、あり得る戻りの記述が存在しないことを示します。
#include <pthread.h> int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); |
pthread_rwlock_trywrlock(3T) は、pthread_rwlock_wrlock() と同様に書き込みロックを適用します。ただし、いずれかのスレッドが現時点で rwlock (読み取り用または書き込み用) を保持している場合、この関数は失敗します。Solaris スレッドについては、「rw_trywrlock(3T)」 を参照してください。
pthread_rwlock_trywrlock() が、初期化されていない読み取り / 書き込みロックに対して呼び出された場合、その結果は不定です。
書き込みのための読み取り / 書き込みロックを待っているスレッドにシグナルが送られた場合、スレッドはシグナルハンドラから戻ると、見かけ上割り込みがなかった場合と同様に、書き込みのための読み取り / 書き込みロック待ちを再開します。
rwlock が参照する読み取り / 書き込みロックオブジェクトの書き込みロックを獲得した場合、戻り値は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
#include <pthread.h> |
pthread_rwlock_unlock(3T) は、rwlock が参照する読み取り / 書き込みロックオブジェクトに保持されたロックを解放します。呼び出しスレッドが rwlock という読み取り / 書き込みロックを保持していない場合、その結果は不定です。Solaris スレッドについては、「rw_unlock(3T) 」 を参照してください。
pthread_rwlock_unlock を呼び出して読み取り / 書き込みロックオブジェクトから読み取りオブジェクトを解放しても、この読み取り / 書き込みロックオブジェクトに他の読み取りロックが保持されている場合、読み取り / 書き込みロックオブジェクトは読み取りにロックされたままになります。pthread_rwlock_unlock() が、呼び出しスレッドによる最後の読み取りロックを解放すると、呼び出しスレッドはこのオブジェクトの所有者でなくなります。pthread_rwlock_unlock() がこの読み取り / 書き込みロックオブジェクトの最後の読み取りロックを解放すると、読み取り / 書き込みロックオブジェクトはロックが解除され、所有者のない状態になります。
pthread_rwlock_unlock() を呼び出し、読み取り / 書き込みロックオブジェクトから書き込みオブジェクトを解放すると、読み取り / 書き込みロックオブジェクトはロックが解除され、所有者のない状態になります。
pthread_rwlock_unlock() を呼び出した結果として読み取り / 書き込みロックオブジェクトがロック解除されたときに、複数のスレッドが書き込みのための読み取り / 書き込みロックオブジェクトの獲得を待っている場合は、スケジューリング方針を使用して、書き込みのための読み取り / 書き込みロックオブジェクトを獲得するスレッドが決定されます。また、複数のスレッドが読み取りのための読み取り / 書き込みロックオブジェクトの獲得を待っている場合も、スケジューリング方針を使用して、読み取りのための読み取り / 書き込みロックオブジェクトを獲得するスレッドの順番が決定されます。さらに、複数のスレッドが読み取りロックと書き込みロック両方のために rwlock にブロックされている場合は、読み取り側と書き込み側のどちらが先にロックを獲得するのかは規定されていません。
pthread_rwlock_unlock() が、初期化されていない読み取り / 書き込みロックに対して呼び出された場合、その結果は不定です。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
#include <pthread.h> int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; |
pthread_rwlock_destroy(3T) は、rwlock が示す読み取り / 書き込みロックオブジェクトを削除し、このロックで使用されていたリソースを解放します。削除したオブジェクトを、pthread_rwlock_init() の呼び出しによって再び初期化する前に使用した場合、その結果は不定です。実装によっては、pthread_rwlock_destroy() は、rwlock が参照するオブジェクトに不正な値を設定する場合もあります。いずれかのスレッドが rwlock を保持しているときに pthread_rwlock_destroy() を呼び出した場合の結果は不定です。初期化されていない読み取り / 書き込みロックを削除しようとした場合に発生する動作も不定です。また、削除された読み取り / 書き込みロックオブジェクトは、再度 pthread_rwlock_init() で初期化できます。削除した読み取り / 書き込みロックオブジェクトを初期化せずに参照した場合も不定です。Solaris スレッドについては、「rwlock_destroy(3T) 」 を参照してください。
正常終了時は 0 です。それ以外の戻り値は、エラーが発生したことを示します。
今までに説明した同期プリミティブは、プロセスの境界を越えて使用するように設定できます。具体的には次のようにします。まず、その同期変数の領域が共有メモリーに確保されるようにし、次に該当する init() ルーチンを呼び出しますが、これはそのプリミティブをプロセス間共有属性で初期化した後に行います。
例 4-17 は、前述の「生産者 / 消費者」問題の生産者と消費者をそれぞれ別のプロセスで表現したものです。メインルーチンは、0 に初期化されたメモリーを自分のアドレス空間にマッピングし、子プロセスと共有します。
子プロセスが 1 つ生成され、消費者の処理が実行されます。親プロセスは生産者の処理を実行します。
この例では、生産者と消費者を呼び出す各駆動ルーチンも示しています。producer_driver() は stdin から文字を読み込み、producer() を呼び出します。consumer_driver() は consumer() を呼び出して文字を受け取り、stdout に書き出します。
例 4-17 のデータ構造は、条件変数による「生産者 / 消費者」の例のデータ構造 (例 4-4 を参照) と同じです。2 つのセマフォでそれぞれ、いっぱいになったバッファ数と未使用バッファ数を表します。これらのセマフォは、未使用バッファができるまで生産者を待たせ、バッファがいっぱいになるまで消費者を待たせます。
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 増やすことができます。
読み取り / 書き込みロックを使用すると、保護されたリソースに対する、並行する複数の読み取り操作や排他的な書き込み操作ができます。読み取り / 書き込みロックは単一の実体で、読み取りモードまたは 書き込み モードでロック可能です。リソースを変更するには、まずスレッドは排他書き込みロックを取得する必要があります。排他書き込みロックは、すべてのロックが解放されるまで使用できません。