一般的なスレッド: POSIX スレッドの説明: 第2回

mutex というちょっとしたもの

POSIX スレッドは、コードの応答性とパフォーマンスを向上させる優れた方法です。3 回シリーズの第 2 回である今回の記事では、mutex というちょっとした優れた手段により、スレッド化されたコードの整合性を保つ方法について、Daniel Robbins が説明します。

Daniel Robbins (drobbins@gentoo.org), President/CEO, Gentoo Technologies, Inc.

Daniel Robbins氏は、ニューメキシコ州アルバカーキーに住んでいます。彼は、Gentooプロジェクトのチーフ・アーキテクト、Gentoo Technologies Inc. の社長/CEOです。著書に、Macmillanから出版されているCaldera OpenLinux Unleashed、SuSE Linux Unleashed、Samba Unleashed があります。Daniel氏は、小学2年のとき初めてLogoプログラム言語や、中毒になる恐れのあったPac Manに出会って以来、何らかの形でコンピューターに関係してきています。これで、彼がなぜSONY Electronic Publishing/Psygnosisでリード・グラフィック・アーチストを務めているかが分かるでしょう。愛妻Maryさんや、生まれたばかりの愛娘Hadassahちゃんとの時間をとても大切にしています。彼の連絡先はdrobbins@gentoo.org です。



2000年 8月 01日

わたしを mutex してください

前回の記事では、予想外の異常なことを行うスレッド化されたコードについて説明しました。2 つのスレッドが 1 つのグローバル変数をそれぞれ 20 回ずつ増分するというものでした。予想では、変数は最終的に値 40 になるはずでしたが、実際には値 21 で終わってしまいました。何が起こったのでしょうか。一方のスレッドが実行した増分を、もう一方のスレッドが繰り返し「無効にした」ため、問題が起こったのです。mutex を使って問題を解決した修正コードを見てみましょう。

thread3.c
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;
void *thread_function(void *arg) {
  int i,j;
  for ( i=0; i<20; i++ ) {
    pthread_mutex_lock(&mymutex);
    j=myglobal;
    j=j+1;
    printf(".");
    fflush(stdout);
   sleep(1);
    myglobal=j;
    pthread_mutex_unlock(&mymutex);
  }
  return NULL;
}
int main(void) {
  pthread_t mythread;
  int i;
  if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
    printf("error creating thread.");
    abort();
  }
  for ( i=0; i<20; i++) {
    pthread_mutex_lock(&mymutex);
    myglobal=myglobal+1;
    pthread_mutex_unlock(&mymutex);
    printf("o");
    fflush(stdout);
    sleep(1);
  }
  if ( pthread_join ( mythread, NULL ) ) {
    printf("error joining thread.");
    abort();
  }
  printf("\nmyglobal equals %d\n",myglobal);
  exit(0);
}

説明の時間

上記のコードと前回の記事のバージョンを比較するなら、pthread_mutex_lock() と pthread_mutex_unlock() という呼び出しが追加されていることに気付くでしょう。これらの呼び出しは、スレッド化されたプログラムにとって大いに必要な機能を実行します。これらの呼び出しは相互的排他 (mutual exclusion) を実現します (これが名前の由来です)。2 つのスレッドが同時に同じ mutex にロックをかけることはできません。

mutex は次のようにして機能します。スレッド "b" がある mutex をロックしている間に、スレッド "a" が同じ mutex にロックをかけようとすると、スレッド "a" はスリープ状態に入ります。スレッド "b" が (pthread_mutex_unlock() 呼び出しを介して) mutex を解放すると、即座にスレッド "a" が同じ mutex をロックできるようになります (つまり、ロックされた mutex とともに pthread_mutex_lock() が戻ります)。同様に、スレッド "a" の保留中にスレッド "c" が mutex にロックをかけようとすると、スレッド "c" も一時的にスリープ状態に入ります。すでにロックされている mutex に対して pthread_mutex_lock() を呼び出すスレッドはすべて、スリープ状態に入り、その mutex にアクセスする順番を「列を作って待つ」ことになります。

pthread_mutex_lock() と pthread_mutex_unlock() は通常、データ構造体を保護するために使用されます。つまり、スレッドをロックまたはロック解除することにより、あるデータ構造にアクセスできるスレッドを 1 つだけに限定することができます。お気付きかもしれませんが、POSIX スレッド・ライブラリーでは、ロック解除されている mutex をロックしようとするスレッドは、スリープ状態に入らずにロックを許可されます。

読者に楽しんでいただけるよう、4 人の znurt が上記の pthread_mutex_lock() 呼び出しのシーンを再現します。
読者に楽しんでいただけるよう、4 人の znurt が上記の pthread_mutex_lock() 呼び出しのシーンを再現します。

このイメージの中で mutex をロックしているスレッドは、自分がアクセスしているのと同じ複合データ構造体に、他のスレッドが同時に手を出しているのではないかと心配する必要はありません。データ構造体は、mutex のロックが解除されるまで、事実上「凍結」しています。pthread_mutex_lock() および pthread_mutex_unlock() 呼び出しは、変更中または読み取り中の特定の共用データの前後に掲げられた「工事中」の標識のようなものだと言えます。これらの呼び出しは、他のスレッドに対し、スリープ状態に入って、自分の番が来るまで mutex をロックするのを待つよう指示する警告のようなものです。当然、これは、特定のデータ構造体を読み書きするたび 、その前後に pthread_mutex_lock() と pthread_mutex_unlock() への呼び出しを配した場合にのみ当てはまります。


一体なぜ mutex なのか

面白いですね。しかし、なぜ、スレッドをよりによってスリープ状態にしなければならないのでしょうか。スレッドの最大の利点は、それぞれ独立して働けること、多くの場合は同時にそうできるということではなかったでしょうか。そう、そのとおりです。しかし、ちょっとしたスレッド・プログラムであれば、必ず mutex を少しは使う必要があるのです。その理由を理解するために、もう一度プログラムの例を見てみましょう。

thread_function() を一見すれば、mutex がループの最初でロックされ、ループの最後で解放されていることに気付かれるでしょう。このプログラム例では、mymutex は myglobal の値を保護するために使用されています。thread_function() を注意深く調べてみると、増分コードが、myglobal をローカル変数にコピーし、ローカル変数を増分し、1 秒間スリープし、その後で初めてローカル値を myglobal に書き戻していることが分かります。mutex を使わないとしたら、thread_function() が眠っている 1 秒の間にメイン・スレッドが myglobal を増分した場合、thread_function() は、目覚めてから、増分された値を上書きしてしまうでしょう。mutex を使えば、そうしたことは起こりません。(読者が不思議に思うことのないよう、1 秒の遅延を組み入れて、間違った結果が生じるようにしました。thread_function() が myglobal にローカル値を書き戻す前に 1 秒間スリープしなければならない理由は、特にありません。) mutex を使った新しいプログラムでは、次に示す望ましい結果が得られます。

$ ./thread3
o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooo
myglobal equals 40

この極めて重要な概念をさらに調べるため、プログラムの中の増分コードを見てみましょう。

thread_function() の増分コード:
    j=myglobal;
    j=j+1;
    printf(".");
    fflush(stdout);
    sleep(1);
    myglobal=j;
メイン・スレッドの増分コード:
    myglobal=myglobal+1;

このコードがシングル・スレッドのプログラムであれば、thread_function() のコードはその全体が実行されるものと期待できます。その実行の後で、メイン・スレッドのコードが実行される (あるいはその逆) ことになります。mutex を使わないスレッド化プログラムの場合、コードが次の順序で実行される可能性があります (sleep() のおかげで、実際にこのように実行されます)。

    thread_function() スレッド	メイン・スレッド
    j=myglobal;
    j=j+1;
    printf(".");
    fflush(stdout);
    sleep(1);		        myglobal=myglobal+1;
    myglobal=j;

上に示した順序でコードが実行されると、メイン・スレッドが myglobal に加えた変更は上書きされてしまいます。プログラムの終了時点で得られる値は、間違ったものになります。ポインターを操作している場合だったら、segfault を生じるところです。thread_function() スレッドは、そのすべての命令を正しい順序で実行する必要があるのです。thread_function() 自体が何か間違ったことをするというわけではありません。問題は、事実上同時に同じデータ構造体に対して別の変更を加えるスレッドが、もう 1 つあるということです。


スレッドの内部 1

mutex を使うべき場所を割り出す方法を説明する前に、スレッド内部の働きについて少し説明します。最初の例を挙げます。

たとえば、1 つのメイン・スレッドが 3 つの新しいスレッド ("a"、"b"、"c") を作成するとします。これらのスレッドは、"a"、"b"、"c" の順番に作成されるものとします。

pthread_create( &thread_a, NULL, thread_function, NULL);
	pthread_create( &thread_b, NULL, thread_function, NULL);
	pthread_create( &thread_c, NULL, thread_function, NULL);

最初の pthread_create() 呼び出しが完了したなら、スレッド "a" について、それが存在しているか、あるいはすでに完了して現在は停止中であるかのどちらかであると見なせます。2 番目の pthread_create() 呼び出しが完了したなら、メイン・スレッドとスレッド "b" は両方とも、スレッド "a" は存在している (あるいは停止している) ものと見なせます。

しかし、2 番目の create() 呼び出しが戻るやいなや、メイン・スレッドは、最初に実行を開始したのが (a と b の) どちらのスレッドであるかを想定できなくなります。スレッドは両方とも存在しているものの、それらのスレッドに CPU 時間をスライスして与えるのは、カーネルおよびスレッド・ライブラリーの責任になります。そして、どのスレッドが先行するかということに関するきちんとした規則はありません。この場合、スレッド "a" がスレッド "b" に先立って実行を開始する可能性が大ですが、その保証があるわけではありません。これは、マルチプロセッサー・マシンの場合に特にそう言えます。スレッド "b" に先立ってスレッド "a" が実際にそのコードの実行を開始する、との仮定の元にコードを書くなら、出来上がるプログラムは時間を 99% 使用するものとなるでしょう。さらに悪いことに、自分のマシンで時間を 100% 使用し、クライアントの 4 プロセッサー・サーバーで使用する時間は 0% ということになりかねません。

この例から学べるもう 1 つの点は、スレッド・ライブラリーは、それぞれのスレッドごとにコード実行の順番を保持するということです。つまり、上記の 3 つの pthread_create() 呼び出しは、実際には、その出現の順番で実行されるということです。メイン・スレッドから見れば、これらのコードはすべて順番に実行されます。これを利用して、スレッド化されたプログラムの一部を最適化できる場合があります。たとえば、上記の例において、スレッド "c" は、スレッド "a" および "b" を、実行中、あるいはすでに終了したものと見なすことができます。スレッド "a" および "b" がまだ作成されていない可能性を心配する必要はありません。この論理を使って、スレッド化されたプログラムを最適化できます。


スレッドの内部 2

よろしい、では、仮想の例をもう 1 つ挙げます。次のコードを実行する一群のスレッドがあるとします。

myglobal=myglobal+1;

増分のたびに、事前に mutex をロックし、後からロックを解除する必要があるでしょうか。「必要ない」とおっしゃる方もあるでしょう。結局のところ、コンパイラーが上記の指定を単一のマシン・インストラクションにコンパイルする可能性が大です。ご存じのとおり、単一のマシン・インストラクションの「途中」に割り込むことはできません。ハードウェア割り込みでさえ、マシン・インストラクションの原子性に沿って行われます。この傾向を考えると、pthread_mutex_lock() 呼び出しと pthread_mutex_unlock() 呼び出しをまったく省きたくなります。しかし、そうしてはなりません。

わたしは臆病なのでしょうか。そうではありません。まず、自分自身でマシン・コードを調べるのでない限り、上記の指定が単一のマシン・インストラクションにコンパイルされると決めてかかるべきではありません。増分が原子的に行われるようインライン・アセンブリーを挿入するにせよ、コンパイラーを自分で書くにせよ、問題が起こる可能性は依然として残ります。

理由は次のとおりです。単一のインライン・アセンブリー命令コードは、単一プロセッサー・マシンではすばらしい働きをするでしょう。増分はそれぞれ原子的に生じ、望む結果を得られる可能性が大きいと言えます。しかし、マルチプロセッサー・マシンとなると話は別です。マルチ CPU マシンの場合、2 つのプロセッサーが上記の指定をほとんど同時に (あるいはまったく同時に) 実行することになります。そして、このメモリーの変更は、L1 から L2 へ、そしてメイン・メモリーへと順番に伝わっていかなければなりません。(対称型マルチプロセッサー・マシンとは、単にプロセッサーが追加されているだけのマシンではありません。RAM へのアクセスを仲裁するための、特別なハードウェアも備えたマシンです。) 結局のところ、どの CPU がメイン・メモリーへの書き込みのレースに「勝つ」のかは分かりません。予測可能なコードを書くためには、mutex を使用することです。mutex は「メモリーのバリア」を挿入します。これは、スレッドが mutex をロックする順番に従って、メイン・メモリーへの書き込みが行われるようにします。

メイン・メモリーを 32 ビット・ブロックに更新する対称型マルチプロセッサー・アーキテクチャーを考えてみてください。64 ビット整数を mutex なしで増分する場合、最初の 4 バイトは 1 つの CPU から来て、次の 4 バイトは別の CPU から来るということが起こりえます。何てことでしょう。中でも最悪なのは、技法がお粗末だと、長時間かけたプログラムが一瞬で飛んだり、午前 3 時に重要なクライアント・システムでプログラムが飛んだりすることがある、ということです。David R. Butenhof は、mutex を使用しない置換として利用可能なものを、その著書「Programming with POSIX Threads」の中で説明しています (この記事の末尾の「参考文献」を参照してください)。


多数の mutex

mutex が多すぎると、コードに並行性がまったくなくなり、単一スレッドのソリューションよりも実行速度が遅くなるでしょう。逆に少なすぎれば、コードに奇妙で厄介なバグが現れます。ありがたいことに、その間を取るという方法があります。まず第一に、mutex は「共用データ」へのアクセスを直列化するために使用するものです。非共用データのために mutex を使用しないでください。また、プログラムの論理により、特定のデータ構造体に一度にアクセスできるスレッドが 1 つだけに限られている場合にも、使用しないでください。

第二に、共用データを使用する場合は、読み取りと書き込みの両方に mutex を使用してください。読み取りセクションと書き込みセクションを pthread_mutex_lock() と pthread_mutex_unlock() で囲むか、プログラムのインバリアントが一時的に崩れるときにいつでもこれらを使用してください。コードを単一のスレッドの観点から眺めることを学び、プログラム内の個々のスレッドがメモリーを一貫した分かりやすい方法で見られるようにしてください。mutex のこつをつかんで自分のコードを書けるようになるには時間かかかるかもしれませんが、それはすぐに身に着き、「あまり」考えないでもそれを適正に使えるようになるでしょう。


呼び出しの使用: 初期化

よろしい、それではここで、mutex を使用するさまざまな方法すべてを調べることにしましょう。まず、初期化から始めることにします。thread3.c の例では、静的な初期化の方式を使用しました。これには、pthread_mutex_t 変数を宣言して、これに定数 PTHREAD_MUTEX_INITIALIZER を割り当てることが含まれます。

pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;

これは非常に簡単です。しかし、mutex を動的に作成することもできます。コードが malloc() を使用して新しい mutex を割り振る場合は必ず、この動的な方式を使用してください。この場合、静的な初期化の方式ではなく、ルーチン pthread_mutex_init() を使用するべきです。

int pthread_mutex_init( pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr)

ご覧のとおり、pthread_mutex_init は、mutex として初期化する、割り振り済みのメモリー領域を指すポインターを受け入れます。2 番目の引数として、オプショナルの pthread_mutexattr_t ポインターを受け入れることもできます。この構造体は、mutex のさまざまな属性を設定するために使用できます。しかし、通常、そのような属性は必要ないので、NULL を指定するのが普通です。

pthread_mutex_init() を使って初期化した mutex は、必ず pthread_mutex_destroy() を使って破棄する必要があります。pthread_mutex_destroy() は pthread_mutex_t へのポインターを受け入れ、mutex の作成時に割り振られたあらゆるリソースを解放します。pthread_mutex_t を保管するときに使用されたメモリーを、pthread_mutex_destroy() が解放することはないことに注意してください。プログラマー自身の責任で、メモリーを free() する必要があります。pthread_mutex_init() と pthread_mutex_destroy() は両方とも、成功するとゼロを戻すということにも留意してください。


呼び出しの使用: ロック

pthread_mutex_lock(pthread_mutex_t *mutex)

pthread_mutex_lock() は、ロックする mutex への単一のポインターを受け入れます。mutex が既にロックされている場合、呼び出し元はスリープ状態に入ります。関数が戻ると、呼び出し元は (当然) 目を覚まし、ロックを保持することになります。この呼び出しも、成功するとゼロ、失敗すると非ゼロのエラー・コードを戻します。

pthread_mutex_unlock(pthread_mutex_t *mutex)

pthread_mutex_unlock() は pthread_mutex_lock() を補完し、スレッドがすでにロックした mutex のロックを解除します。ロックした mutex は、安全な範囲でできるだけ早くロック解除する必要があります (パフォーマンスの向上のため)。ロックしていない mutex は絶対にロック解除してはなりません (さもないと、pthread_mutex_unlock() 呼び出しが失敗し、非ゼロの EPERM 戻り値が戻されます)。

pthread_mutex_trylock(pthread_mutex_t *mutex)

この呼び出しは、スレッドが (mutex がその時点でロックされているために) 何か別のことをしている間に、mutex をロックする場合に便利です。pthread_mutex_trylock() を呼び出すと、mutex のロックが試みられます。mutex が現在ロック解除されている状態なら、ロックがかけられ、この関数はゼロを戻します。しかし、mutex がロックされている場合、この呼び出しが妨害を行うことはありません。むしろ、非ゼロの EBUSY エラー値を戻します。そうしたら、自分の仕事に取り掛かって、ロックは後で試行することができます。


条件の待機

mutex はスレッド化されたプログラムにとって必要なツールですが、何でもできるわけではありません。たとえば、スレッドが共用データに特定の条件が生じるのを待っている場合、何が起こるでしょうか。コードが、値に何か変更が加えられることがないか検査しながら、mutex のロックとロック解除を繰り返すかもしれません。それと同時に、他のスレッドが必要な変更を加えることができるよう、mutex のロック解除も素早く行います。しかし、これは恐ろしいアプローチです。なぜなら、このスレッドは、妥当な時間フレームの間、変更を検出するために絶えずループを回す必要があるからです。

呼び出し側のスレッドを、検査と検査の合間に少しだけ (たとえば 3 秒間) スリープ状態にすることもできるかもしれませんが、この場合、スレッド化されたコードが最良の仕方で責任を果たしているとは言えないでしょう。ここで本当に必要なのは、ある条件が満たされるまでの間、スレッドをスリープ状態にしておく方法です。いったん条件が満たされたなら、今度は、その特定の条件が真になるのを待つスレッドを目覚めさせるための方式が必要になります。それができるなら、スレッド化されたコードは本当に効果的なものになり、貴重な mutex ロックを占有することはなくなるでしょう。これこそが、POSIX の条件変数が行えることです。

POSIX 条件変数は、次の記事で取り上げる主題であり、その詳細な使用法は次の記事で説明します。その時、作業班や組み立てラインなどのモデリングを行う、高機能なスレッド化プログラムを作成するための資材がすべて揃うことになります。読者もスレッドに精通してこられたことですし、次の記事ではペースを上げることにします。そうして、次回の記事の最後までに、ある程度の機能を備えたスレッド化プログラムを作成できるようにしたいと望んでいます。さらに、条件の待機についても説明します。では、またお目にかかりましょう。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=229868
ArticleTitle=一般的なスレッド: POSIX スレッドの説明: 第2回
publish-date=08012000