mutex の属性を設定後、mutex そのものを初期化します。以下の関数は、mutex の初期化、削除、ロック、ロック解除、およびブロックしないで行う mutex のロックに使用します。表 4-3 に、この章で説明する mutex ロック操作関数を示します。
表 4-3 相互排他ロック操作ルーチン
デフォルトスケジューリング方針 SCHED_OTHER は、スレッドによるロックの獲得順序を指定していません。複数のスレッドが mutex を待っているときの獲得の順序は不定です。衝突するときは、スレッドを優先順位でブロック解除するというのがデフォルト動作です。
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 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、対応する値を戻します。
プロトタイプ: 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 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。
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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
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 です。それ以外の戻り値は、エラーが発生したことを示します。以下のいずれかの条件が検出されると、この関数は失敗し、次の値を戻します。
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 です。それ以外の戻り値は、エラーが発生したことを示します。以下の条件が検出されると、この関数は失敗し、次の値を戻します。
次に、相互排他ロックを示すコードの一部を示します。
#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 をロックしようとするとデッドロックが発生します。
スレッド 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 は、その方法を示しています。
スレッド 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 つのロックを獲得する場合を説明します。この例では、デッドロックを防ぐために規定された順序でロックします。
typedef struct node1 { int value; struct node1 *link; pthread_mutex_t lock; } node1_t; node1_t ListHead;
この例で使用する片方向リンクのリスト構造体は、各ノードに相互排他ロックを含んでいます。このリストから特定のノードを削除する場合は、最初に ListHead (これが削除されることはない) の位置からリストを辿って目的のノードを探します。
この検索を同時並行的に行われる削除から保護するために、各ノードをロックしてからノードの内容をアクセスしなければなりません。すべての検索が ListHead の位置から開始されるので、常にリストの順序でロックされます。このため、デッドロックは決して発生しません。
目的のノードが見つかった時は、この変更がそのノードと直前のノードの両方に影響を与えるため、両方をロックします。直前のノードのロックが常に最初に獲得されるので、ここでもデッドロックの心配はありません。
例 4-5 は、片方向リンクリストから特定のノードを削除する C コードを示しています。
node1_t *delete(int value) { node1_t *prev, *current; prev = &ListHead; pthread_mutex_lock(&prev->lock); while ((current = prev->link) != NULL) { pthread_mutex_lock(¤t->lock); if (current->value == value) { prev->link = current->link; pthread_mutex_unlock(¤t->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 つのノードに関連付けられると、そのノードと次のノードに対して操作を行います。この状況ではロック階層は適用できません。明らかに階層 (つまり、リンクをたどる順番) が循環的だからです。
typedef struct node2 { int value; struct node2 *link; pthread_mutex_t lock; } node2_t;
例 4-7 では 2 つのノードをロックし、両方のノードに対してある操作を行なっている C コードを示します。
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); }