以下は、perfbook の付録Bの kanda.motohiro@gmail.com による全訳です。perfbook の訳の他の部分は、親文書を参照。
付録B 同期プリミティブ
最も単純な並列プログラム以外は、同期プリミティブが必要です。この付録は、Linuxカーネルの同期プリミティブをほぼベースとするプリミティブのセットの素早い概略を与えます。
なぜLinux?なぜならばそれは、よく知られ、巨大で、入手が簡単な、利用可能な並列コードのボディの一つだからです。私たちは、学習にとって、コードを読むことは、どちらかと言えば、コードを書くことよりも重要だと信じます。なので、Linuxカーネルの本物のコードに似た例を使うことで、私たちは、あなたがこの本の囲いを超えて進んでいく時に、あなたがご自分の学習を続けるためにLinuxを使うことができるようにしました。
なぜ、LinuxカーネルAPIに正確に従うのでなくて、ほぼベースとするのでしょう?まず、Linux APIは時とともに変わります。なので、それを正確に追跡しようという全ての試みは、関連する人全てに完全な不満足をもたらして終わることになるでしょう。二つ目に、LinuxカーネルAPIの多くのメンバは、本番品質のオペレーティングシステムカーネルで使うために特殊化されています。この特殊化は複雑さをもたらします。それは、Linuxカーネル自身にとっては絶対に必要です。しかしそれは、私たちがこれから、SMPとリアルタイムの設計原理と実践を示すために使おうとする「おもちゃの」プログラムにとっては、しばしば、役に立つよりは面倒を起こします。例えば、メモリ枯渇のようなエラー条件を適切に判定することは、Linuxカーネルにおいては、「必須」です。しかし、「おもちゃの」プログラムにおいては、単純にプログラムを abort() させて、問題をなおし、再実行するのは、完全に許されることです。
最後に、このAPIとほとんどの本番レベルのAPIの間に、自明なマッピング層を実装するのは可能でしょう。pthreads 実装があります (CodeSamples/ api-pthreads/api-pthreads.h)。そして、LinuxカーネルモジュールAPIを作成するのは難しくないでしょう。
クイッククイズB.1
同期プリミティブなしに書ける並列プログラムの例をあげなさい。
以下の節は、一般的に使われる同期プリミティブのクラスを説明します。
B.1節は、組織化と初期化プリミティブをカバーします。B.2節は、スレッド作成、破壊、そして制御プリミティブを紹介します。B.3節は、ロックプリミティブです。B.4節は、スレッドごとと、CPUごと変数です。そして、B.5節は、いろいろなプリミティブの相対的性能の概略を与えます。
B.1 組織化と初期化
B.1.1 smp_init():
他の全てのプリミティブを呼ぶ前に、smp_init() を呼ばなくてはいけません。
B.2 スレッド作成、破壊、そして制御
このAPIは、「スレッド」に焦点を当てます。それは、制御の中心点です。
脚注
類似のソフトウエア構成物に対して多くの他の名前があります。それは、「プロセス」、「タスク」、「ファイバー」、「イベント」などを含みます。これら全てに、類似の設計基準が適用されます。
そのようなそれぞれのスレッドは、thread_id_t 型の識別子を持ち、ある時点で走っている二つのスレッドが同じ識別子を持つことはありません。スレッドは、スレッドごとのローカル状態、それにはプログラムカウンタとスタックを含みます、以外の全てを共用します。
脚注
循環定義ですけども。
スレッドAPIを、図B.1に示します。メンバは以下の節で説明します。
int smp_thread_id(void)
thread_id_t create_thread(void *(*func)(void *), void *arg)
for_each_thread(t)
for_each_running_thread(t)
void *wait_thread(thread_id_t tid)
void wait_all_threads(void)
図B.1 スレッドAPI
B.2.1 create_thread()
create_thread() プリミティブは新しいスレッドを作ります。create_thread() の最初の引数で指定した func から、新しいスレッドの実行を始めます。そしてそれには、create_thread() の二つ目の引数で指定した引数を渡します。新しく作られたスレッドは、func で指定した開始関数から戻った時に終了します。create_thread() プリミティブは、新しく作られた子スレッドに対応する thread_id_t を返します。
このプリミティブは、NR_THREADS 以上のスレッドが作成されたら、そのプログラムを中断します。そのプログラムを走らせるために暗黙的に作られた一つを含みます。NR_THREADS は、変更可能なコンパイル時の定数です。ただ、システムによっては、許されるスレッド数の上限を持つものもあります。
B.2.2 smp_thread_id()
create_thread() が返す thread_id_t はシステム依存のため、smp_thread_id() プリミティブはその要求を出すスレッドに対応するスレッドインデックスを返します。このインデックスは、そのプログラムが開始してから存在したことのあるスレッドの最大数よりも小さいことが保証されます。このためそれは、ビットマスク、配列インデックスなどに便利です。
B.2.3 for_each_thread()
for_each_thread() マクロは、存在する全てのスレッドをループします。それには、もし作成されたら存在するであろう全てのスレッドを含みます。このマクロは、 B.4 節で見るように、スレッドごと変数を扱うのに便利です。
B.2.4 for_each_running_thread()
for_each_running_thread() マクロは、現在存在するスレッドだけをループします。必要ならば、スレッド作成と削除と同期するのは、呼び出し元の責任です。
B.2.5 wait_thread()
wait_thread() プリミティブは、それに渡された thread_id_t で指定されるスレッドの完了を待ちます。これはいかなる意味でも、指定されたスレッドの実行に影響を与えません。そうでなく、これは単純にそれを待ちます。wait_thread() は、対応するスレッドが返した値を返すことに注意下さい。
B.2.6 wait_all_threads()
wait_all_threads() プリミティブは、現在実行中の全てのスレッドの完了を待ちます。必要ならば、スレッド作成と削除と同期するのは、呼び出し元の責任です。しかしこのプリミティブは通常は、実行の後始末と終了を行うために使われるので、そのような同期は通常は不要です。
B.2.7 使用例
図 B.2 は、 hello-world 的な子スレッドの例を示します。以前に述べたように、それぞれのスレッドは自分自身のスタックを確保されます。なので、それぞれのスレッドは、自分自身の arg 引数と myarg 変数を持ちます。それぞれの子は、単純にその引数と自分の smp_thread_id() を表示して終了します。7行目の return 文がスレッドを終わらせることに注意下さい。 このスレッドに対して wait_thread() を発行した人には NULL を返します。
1 void *thread_test(void *arg)
2 {
3 int myarg = (int)arg;
4
5 printf("child thread %d: smp_thread_id() = %d\n",
6 myarg, smp_thread_id());
7 return NULL;
8 }
図 B.2 子スレッドの例
親プログラムを、図B.3 に示します。それは、6行目でスレッドシステムを初期化するために、smp_init() を呼び、7から14行目で引数を解析し、15行目で自分の存在を表明します。16と17行目で、指定された数の子スレッドを作成し、18行目で、それらが完了するのを待ちます。wait_all_threads() はスレッドの戻り値を捨てることに注意下さい。今の場合それは全て NULLであり、あまり面白いものではありません。
B.3 ロック
ロック APIを図 B.4 に示します。それぞれの API 要素は以下の節で説明します。
void spin_lock_init(spinlock_t *sp);
void spin_lock(spinlock_t *sp);
int spin_trylock(spinlock_t *sp);
void spin_unlock(spinlock_t *sp);
図 B.4 ロック API
B.3.1 spin_lock_init()
spin_lock_init() プリミティブは、指定された spinlock_t 変数を初期化します。この変数を他の全てのスピンロックプリミティブに渡す前にこれを呼ばなくてはいけません。
B.3.2 spin_lock()
spin_lock() プリミティブは、指定されたスピンロックを取ります。必要ならば、そのスピンロックが使用可能となるまで待ちます。Linux カーネル のようなある環境では、この待ちは、「スピンする」ことを含みます。一方、pthread のような別の環境では、ブロックすることを含みます。
訳注
原文は逆です。コードサンプルでわかるように、pthread ではスピンロックを mutex にマップするのでブロックします。
鍵となる点は、ある時点では一つのスレッドだけがスピンロックを持つことができることです。
B.3.3 spin_trylock()
spin_trylock() プリミティブは、指定されたスピンロックを取ります。しかし、それが直ちに使用可能なときに限ります。それは、スピンロックを取れた時には、真を、そうでないときは偽を返します。
B.3.4 spin_unlock()
spin_unlock() プリミティブは、指定されたスピンロックを放します。他のスレッドはそれを取ることができます。
B.3.5 使用例
mutex と呼ばれるスピンロックを使って、以下のように変数counterを守ることができます。
spin_lock(&mutex);
counter++;
spin_unlock(&mutex);
クイッククイズ B.2:
mutex の保護なしに、変数counter が加算されたらどんな問題が起きますか?
しかし、 spin_lock() と spin_unlock() プリミティブはかなりの性能影響があります。それについてはB.5節にて。
B.4 スレッドごと変数
図B.5 は、スレッドごと変数 API です。このAPIは、グローバル変数のスレッドごとの同等物を提供します。このAPIは、厳密に言うと、不要ですが、コーディングを大いに単純にできます。
DEFINE_PER_THREAD(type, name)
DECLARE_PER_THREAD(type, name)
per_thread(name, thread)
__get_thread_var(name)
init_per_thread(name, v)
図 B.5: スレッドごと変数 API
クイッククイズ B.3:
スレッドごと変数 APIを提供しないシステムにおいて、回避策はありますか?
B.4.1 DEFINE_PER_THREAD()
DEFINE_PER_THREAD() プリミティブは、スレッドごと変数を定義します。不幸にも、Linuxカーネルの DEFINE_PER_THREAD() が許す方法で initializer を提供するのは不可能です。しかし、簡単に実行時の初期化を可能とするinit_per_thread() プリミティブがあります。
B.4.2 DECLARE_PER_THREAD()
DECLARE_PER_THREAD() プリミティブは、C の意味では定義ではなくて宣言です。なので、DECLARE_PER_THREAD() は、どこか他のファイルで定義されたスレッドごと変数をアクセスするために使うことができます。
B.4.3 per_thread()
per_thread() プリミティブは、指定されたスレッドの変数をアクセスします。
B.4.4 __get_thread_var()
__get_thread_var() プリミティブは、現在スレッドの変数をアクセスします。
B.4.5 init_per_thread()
init_per_thread() プリミティブは、指定された変数の全てのスレッドごとのインスタンスを指定された値に設定します。
B.4.6 使用例
とてもひんぱんに加算されるけど、とてもまれに読まれるカウンタがあるとします。B.5 節で明らかになるように、そのようなカウンタは、スレッドごと変数を使って実装するのが便利です。そのような変数を定義するにはこうします。
DEFINE_PER_THREAD(int, counter);
カウンタはこのように初期化しなければいけません。
init_per_thread(counter, 0);
スレッドはこのカウンタの自分のインスタンスを加算するにはこうします。
__get_thread_var(counter)++;
カウンタの値は、すると、そのインスタンスの合計です。そのカウンタの値のスナップショットはこのように集めることができます。
for_each_thread(i)
sum += per_thread(counter, i);
繰り返しますが、他の機構を使っても、同様の効果を得ることができます。しかし、スレッドごと変数は、簡単で高性能です。
B.5 性能
B.3節に示すロックを取る加算の性能を、スレッドごと変数(B.4節を参照)や、慣用的な加算("counter++" のような)と比べるのは、学ぶことが多いです。
性能の違いは、控えめに言って、とても大きいです。この本の目的は、あなたがそのような性能の落とし穴を避けながら、SMPプログラム、もしかするとリアルタイム応答を持つ、を書くのを助けることです。次の節は、そのプロセスを、この性能の欠如の理由のいくらかを説明することから始めます。
以上