4章 仕事のツール
この章は、並列プログラミング業界の基本的なツールのいくつかをざっと紹介します。主に、 Linux のようなオペレーティングシステムの上で動いているユーザアプリケーションから使えるものに焦点を当てます。4.1節はスクリプト言語から初めます。4.2節は POSIX API でサポートされているマルチプロセス並列性を説明し、POSIX スレッドについて触れます。4.3節はアトミック操作を説明し、4.4節は Linux カーネル内での類似の操作を説明します。最後に、4.5節は仕事を完成させるためにツールを選ぶことの助けをします。 この章は、ごく簡単な紹介しかしないことに注意下さい。より詳しくは、言及した参考文献を参照下さい。また、これらのツールの最も良い使い方についての情報は、以降の章を参照下さい。 4.1 スクリプト言語
Linux シェルスクリプト言語は、並列性を管理する、単純でありかつ効率的な方法を提供します。例えば、あなたが compute_it プログラムを持っていて、2つの異なる引数のセットを使って実行する必要があるとします。これは、UNIX シェルスクリプトを使って、このようにできます。
1 compute_it 1 > compute_it.1.out &
2 compute_it 2 > compute_it.2.out &
3 wait
4 cat compute_it.1.out
5 cat compute_it.2.out
1と2行目でこのプログラムの2つのインスタンスを起動します。それらの出力を2つの別々のファイルにリダイレクトします。& 文字は、シェルに、このプログラムの2つのインスタンスをバックグラウンドで走らせるように指示します。3行目で両方のインスタンスが完了するのを待ち、4と5行目でそれらの出力を表示します。結果となる実行を、図4.1に示します。compute_it の2つのインスタンスは並列に実行し、wait はその両方が完了した後に完了し、cat の2つのインスタンスはシーケンシャルに実行します。
クイッククイズ4.1
でもこの愚かなシェルスクリプトは、実際の並列プログラムではありません!なぜこんな自明なものに関わるのですか???
クイッククイズ4.2
並列シェルスクリプトを作るより簡単な方法はありますか?あるなら、どうやって?ないなら、それはなぜ?
もう一つの例として、make ソフトウェアビルドスクリプト言語は、-j オプションを提供し、ビルド処理でどれだけの並列性を使うべきかを指定できます。
例えば、Linux カーネルをビルドする時に、make -j4 をタイプすると、最大4つの並列コンパイルが同時に行われることを指定します。
これらの単純な例で、あなたが、並列プログラミングは常に複雑で難しい物である必要はないのだと納得してくれると嬉しいです。
クイッククイズ4.3
でも、スクリプトベースの並列プログラミングがそんなに簡単なら、なぜ、それ以外のものにかまうのです?
4.2 POSIX マルチプロセッシング
この節は、pthread [Ope97] を含む POSIX 環境の表面をひっかきます。この環境は広く実装されており、すぐに使用可能だからです。4.2.1節は POSIX fork() と関連するプリミティブを簡単に見ます。4.2.2節は、スレッド作成と破壊に触れます。4.2.3節は、POSIX ロックの簡単な概要を説明し、最後に4.2.4節は、多くのスレッドが読む一方、ごくまれにしか更新されないデータに使うことができる特別なロックの説明をします。
4.2.1 POSIX プロセス作成と破壊
プロセスは、fork() プリミティブを使って作成し、kill() プリミティブを使って破壊することができます。プロセスは、exit() プリミティブを使って自分を破壊することもできます。fork() プリミティブを実行しているプロセスは、新しく作成されたプロセスの「親」と言われます。親は、子を、wait() プリミティブを使って待つことができます。 この節の例はとても単純であることに注意下さい。これらのプリミティブを使う実世界のアプリケーションは、シグナル、ファイル記述子、共用メモリセグメント、そして他の多くの資源を操作する必要があるでしょう。さらに、アプリケーションによっては、ある子が終了した時に特定の動作をする必要があるでしょう。子が終了した理由も気にしなくてはいけません。これらの配慮はもちろん、コードを相当に複雑にします。詳しくは、この題材についての多くの教科書 [Ste92] を参照下さい。
fork() は成功したら、2度、戻ります。一つは親で、もう一つは、子です。コール元は、fork() の戻り値でどちらかわかります。図4.2 (forkjoin.c) に示すとおりです。1行目は、fork() プリミティブを実行し、戻り値をローカル変数 pid に退避します。2行目は pid がゼロか見て、そうならばこれは子なので、3行目に続きます。以前に述べたように、子は exit() プリミティブで終了することができます。そうでないならば、これは親なので、4行目で fork() プリミティブのエラー戻り値をチェックします。そして、そうならば5から7行目でエラーを表示して終了します。そうでないならば、fork() は成功したので、親は9行目を実行します。pid は、子のプロセスIDを持っています。 親は、wait() プリミティブを使って、子が完了するのを待つことができます。しかし、このプリミティブの使い方は、シェルスクリプトでそれに対応するものと比べて少し複雑です。wait()の呼び出しは、一つの子プロセスだけを待つからです。なので、wait() を、図4.3 (api-pthread.h)に示す waitall() のような関数にラップするのが一般的です。この waitall() 関数は、シェルスクリプトの wait コマンドに似たセマンティクスを持ちます。6から15行にまたがるループのそれぞれのパスは、一つの子プロセスを待ちます。7行目は wait() プリミティブを発行します。それは、子が終了するまでブロックします。そして、その子のプロセスIDを返します。もし、その代わりにプロセスIDが -1 なら、これは、wait() プリミティブが子を待てなかったことを示します。その場合、9行目で ECHILD errno を調べます。それは、子プロセスがもういないことを示します。なので、10行目でループを抜けます。そうでない場合、11と12行目でエラーを表示して終了します。
クイッククイズ4.4 なぜこの wait() プリミティブはこんなに複雑である必要があるのですか?単純に、シェルスクリプトの wait と同じように動作するようにしたらどうですか?
親と子がメモリを共用しないことに注意するのは、致命的に重要なことです。これは、図4.4(forkjoinvar.c) に示すプログラムで明らかにされます。そこでは、子が6行目でグローバル変数 x に1を設定し、7行目でメッセージを表示して、8行目で終了します。親は14行目から続行します。それは、子を待ち、15行目で、変数 x の自分のコピーが、ゼロのままであることに気が付きます。なので、出力はこうなります。 Child process set x=1 Parent process sees x=0 クイッククイズ4.5 fork() と wait() には、ここで議論していないことがもっとたくさんありますよね? 最も粒度の細かい並列性は、共用メモリを必要とします。それについては、4.2.2節で扱います。とは言え、共用メモリ並列性は、fork-join 並列性に比べて、より大いに複雑になることがあります。
4.2.2 POSIX スレッド作成と破壊
既存のプロセス内でスレッドを作成するには、pthread_create() プリミティブを呼びます。例えば、図4.5 (pcreate.c) の15と16行に示すとおりです。最初の引数は、pthread_t へのポインタで、作成されるスレッドのIDを格納します。2つ目の NULL 引数は、オプショナルな pthread_attr_t へのポインタ、3つ目の引数は、新しいスレッドが呼ぶ関数(今の場合、mythread())、そして最後の NULL 引数は、mythread に渡される引数です。 この例では、mythread() は単純に戻りますが、その代わりに pthread_exit() を呼んでもいいです。 クイッククイズ4.6 図4.5の mythread() 関数が、単純に戻って良いなら、なぜ、pthread_exit() にかまうのですか? 20行目の pthread_join() プリミティブは、fork-join の wait() プリミティブに似ています。それは、tid 変数で指定したスレッドが実行を完了するまでブロックします。スレッドは、pthread_exit() を呼ぶか、あるいは、スレッドのトップレベル関数から戻ると、実行を完了します。スレッドの終了値は pthread_join() の2つ目の引数で渡されたポインタを使って格納されます。スレッドの終了値は、pthread_exit() に渡された値か、あるいは、スレッドのトップレベル関数が戻した値です。それは、問題のスレッドがどのように終了したかに依存します。 図4.5に示すプログラムは、以下の出力を生成します。これは、メモリが、実際に、2つのスレッド間で共用されていることを示します。 Child process set x=1 Parent process sees x=1 このプログラムは、変数 x に、一度に1つのスレッドだけが値を格納することを、注意深く保証していることに注意下さい。あるスレッドがある変数に値を格納している可能性があり、その一方で他のあるスレッドがその同じ変数をロードあるいは格納する状況は全て、「データ競合」と呼ばれます。C 言語は、データ競合の結果が、どのようにであれ合理的であることを何も保証しないので、データを同時に安全にアクセス、変更する何らかの方法が必要です。次の節で議論するロックプリミティブのようなものが。
クイッククイズ4.7 C 言語がデータ競合がある場合に何も保証をしないならば、Linux カーネルはなぜこんなに多くのデータ競合を持つのですか?Linux カーネルは完全にこわれていると言いたいのですか???
4.2.3 POSIX ロック
POSIX 標準は、「POSIX ロック」によってプログラマがデータ競合を避けることを可能とします。POSIX ロックには多くのプリミティブがそろっています。それらのうち最も基本的なのは、pthread_mutex_lock() と pthread_mutex_unlock() です。これらのプリミティブは pthread_mutex_t 型のロックに対してはたらきます。ロックは、静的に宣言し、PTHREAD_MUTEX_INITIALIZER で初期化することができます。あるいは、ダイナミックに確保して、pthread_mutex_init() プリミティブで初期化することもできます。この節のデモ用のコードは、前者を使います。
pthread_mutex_lock() プリミティブは指定されたロックを「取り」、pthread_mutex_unlock() は指定されたロックを「放し」ます。これは「排他的」ロックプリミティブですから、ある時点では、あるロックはただ一つだけのスレッドがそれを「保持」できます。例えば、二つのスレッドが同じロックを同時に取ろうとしたら、二つのうち一つが先にロックを「許可され」、もう片方は最初のスレッドがロックを放すまで待ちます。 クイッククイズ4.8 複数スレッドが同じロックを同時に保持したい時はどうしますか? この排他的ロックの性質は、図4.6 (lock.c) に示すコードで明らかにすることができます。1行目でlock_a という名前のPOSIXロックを定義し、初期化します。2行目で lock_b という名前のロックを同様に定義し、初期化します。3行目は共用変数 x を定義し、初期化します。 5から28行目は、lock_reader() を定義します。これは、arg で指定されたロックを保持したまま、共用変数 x を繰り返し読みます。10行目は、 arg を pthread_mutex_t へのポインタにキャストします。
これは、pthread_mutex_lock() と pthread_mutex_unlock() プリミティブが要求するとおりです。
クイッククイズ4.9 図4.6の5行目の lock_reader() への引数を、単純に pthread_mutex_t へのポインタにしたらどうですか?
12から15行目は、指定された pthread_mutex_t を取り、エラーをチェックして、エラーが起きればプログラムを終了します。16から23行は繰り返し x の値をチェックして、それが変わった時には毎回新しい値を表示します。22行目で1ミリ秒、眠ります。これは、このデモがユニプロセッサマシンでうまく動くようにするためです。24から27行目で pthread_mutex_t を放し、同様にエラーをチェックし、エラーが起きればプログラムを終了します。最後に28行目は NULLを返します。これも、pthread_create() に必要な関数型に合わせるためです。 クイッククイズ4.10 pthread_mutex_t を確保、解放するたびに、4行のコードを書くのは、全く苦痛に満ちています!もっと良い方法はありませんか? 図4.6の31から49行目は lock_writer() です。これは、指定された pthread_mutex_t を保持したまま、定期的に共用変数 x を更新します。lock_reader() と同様、34行目は arg を pthread_mutex_t へのポインタにキャストします。36から39行目は指定されたロックを取り、44から47行目で放します。ロックを持ったまま、40から43行目は共用変数 x を加算します。それぞれの加算の間に、5ミリ秒眠ります。最後に、44から47行目でロックを放します。 図4.7は、lock_reader() と lock_writer() を同じロックを使うスレッドとして実行するコード断片です。2から6行目で lock_reader() を実行するスレッドを作り、7から11行目で lock_writer() を実行するスレッドを作ります。12から19行目で両方のスレッドが完了するのを待ちます。このコード断片の出力はこのようになります。
Creating two threads using same lock:
lock_reader(): x = 0
両方のスレッドが同じロックを使っているので、 lock_reader() スレッドは lock_writer() がロックを保持したまま生成する x の中間値を決して見ることはありません。
クイッククイズ4.11
"x=0" が、図4.7のコード断片の唯一可能な出力なのですか?そうならなぜ?違うなら、それ以外のどんな出力が現れる可能性がありますか?そしてなぜ?
図4.8は、同様のコード断片ですが、異なるロックを使います。 lock_reader() は、lock_ a を使い、lock_writer() は lock_b を使います。 このコード断片の出力はこうなります。
Creating two threads w/different locks:
lock_reader(): x = 0
lock_reader(): x = 1
lock_reader(): x = 2
lock_reader(): x = 3
二つのスレッドは異なるロックを使っているので、お互いに排他せず、同時に走ることができます。なので、 lock_reader() 関数は lock_writer() が格納する x の中間値を見ることができます。
クイッククイズ4.12
異なるロックを使うことは、スレッドがお互いの中間状態を見るという大変な混乱を起こすことがあります。ならば、正しく書かれた並列プログラムは、このような混乱を防ぐために単一のロックを使うように制限されるべきではないですか?
クイッククイズ4.13 図4.8に示したコードで、 lock_reader() は lock_writer() が生成する全ての値を見ることが保証されていますか?なぜ、あるいは、なぜそうでないか?
クイッククイズ4.14
ここでちょっと待ってください!!!図4.7は共用変数 x を初期化していません。なぜ、図4.8では初期化しないといけないのですか?
POSIX排他ロックについては、まだたくさん言うことがありますが、これらのプリミティブは開始点としては良いものですし、実際に多くの場合にそれで十分です。次の節は、POSIX リーダーライターロックを簡単に見ましょう。
4.2.4 POSIX リーダーライターロック
POSIX API は、リーダーライターロックを提供し、それは、pthread_ rwlock_t で表されます。pthread_mutex_t と同様に、pthread_rwlock_t は、PTHREAD_RWLOCK_INITIALIZER によって静的に初期化すること、pthread_rwlock_init() プリミティブによってダイナミックに初期化することもできます。pthread_rwlock_rdlock() プリミティブは、指定された pthread_rwlock_t をリード確保し、pthread_rwlock_ wrlock() はそれをライト確保します。pthread_rwlock_unlock() プリミティブはそれを解放します。ある時点では、一つのスレッドだけがある pthread_rwlock_t ロックをライト確保できます。しかし、複数のスレッドがある pthread_rwlock_t ロックをリード確保するのは可能です。少なくても、現在それをライト確保しているスレッドがいなければ。
あなたが期待される通り、リーダーライターロックは、リードがほとんどの状況のために設計されました。その場合、リーダーライターロックは、排他的ロックが可能なより優れたスケーラビリティを提供できます。排他的ロックは、定義によって、一度に一つのスレッドだけがロックを保持できるからです。一方、リーダーライターロックは任意の多くのリーダーが同時にロックを保持するのを許します。しかし、現実的に、リーダーライターロックがどれだけの追加のスケーラビリティを提供するのか、調べなくてはいけません。
図4.9 (rwlockscale.c) は、リーダーライターロックのスケーラビリティを測る一つの方法を示します。1行目がリーダーライターロックの定義と初期化です。2行目は、holdtime 引数で、それぞれのスレッドがリーダーライターロックを保持する時間を制御します。 3行目は thinktime 引数で、リーダーライターロックを解放してから次に取るまでの時間を制御します。4行目は readcounts を定義し、それぞれのリーダースレッドは、ロックを確保した回数をここに置きます。5行目は nreadersrunning 変数で、全てのリーダースレッドが開始したかを判断します。
7から10行目は goflag で、テストの開始と終了を同期します。この変数は最初に GOFLAG_INIT に設定され、全てのリーダースレッドが開始した後に GOFLAG_RUN に設定され、最後にテスト実行を止めるために GOFLAG_STOP に設定されます。12から41行目は reader() を定義します。これが、リーダースレッドです。18行目で nreadersrunning 変数をアトミックに加算して、このスレッドが走り始めたことを示します。19から21行めはテストが開始するのを待ちます。ACCESS_ONCE() プリミティブはコンパイラに、goflag をループのパスで毎回フェッチするように強制します。そうしないと、コンパイラは goflag の値が決して変わらないと、自分の権限の元で判断するかもしれません。
クイッククイズ4.15
ACCESS_ONCE() をあちこちで使う代わりに、図4.9の10行目で、goflag を単純に volatile と宣言したらどうでしょう?
クイッククイズ4.16
ACCESS_ONCE() はコンパイラだけに影響し、CPUには影響しません。図4.9で、goflag の値が変わったことが、CPU にタイムリーに伝搬することを保証するために、メモリバリアも必要ではないでしょうか?
クイッククイズ4.17
スレッドごと変数、例えば、gcc __thread 記憶域クラスで宣言された変数をアクセスするときにでも、ACCESS_ONCE() を使う必要がありますか?
22から38行目にまたがるループは性能テストを実行します。23から26行目でロックを取り、27から29行目はそのロックを指定された期間、保持します(そして barrier() ディレクティブは、コンパイラが最適化の結果、ループを消してしまうのを防ぎます)。30から33行目はロックを放し、34から36行目は次にロックを取るまでに、指定された期間、待ちます。37行目はこのロック確保を数えます。
39行目はロック確保カウントを、readcounts[] 配列のこのスレッドの要素に移動します。そして、40行目で戻り、このスレッドを終わります。
図4.10は、このテストを、64コアの Power-5 システムで走らせた結果を示します。コアには2つのハードウェアスレッドがありますから、合計で128のソフトウェアから見えるCPUがあります。全てのテストにおいて、thinktime パラメタはゼロです。holdtime パラメタは、1000 (グラフでは“1K”)から、1億(グラフでは“100M”)に変化するように設定されます。実際にプロットされる値は、これです。
N はスレッド数、LN は、N スレッドが確保するロック数、そして、L1 は単一スレッドが確保するロック数です。理想的なハードウェアとソフトウェアスケーラビリティの元では、この値は常に1.0のはずです。
図でわかるように、リーダーライターロックのスケーラビリティは明らかに理想的とは言えません。特に、クリティカルセクションが小さい時にはそうです。なぜ、リード確保がこんなに遅いのでしょう。全ての確保するスレッドは pthread_rwlock_t データ構造を更新しなければいけないことを考えましょう。なので、もし全ての128の実行しているスレッドがリーダーライターロックを同時にリード確保しようとしたら、それらはこの元となる pthread_rwlock_t を一度に一人ずつ更新しなければいけません。一つの幸運なスレッドはそれをほとんど即座にできるでしょう。しかし、最も不運なスレッドは他の全ての127スレッドが自分の更新をするのを待たないといけません。この状況は、CPUを増やすと悪くなるばかりです。
クイッククイズ4.18
単一CPUのスループットと比べるのは、少し、厳しすぎませんか?
クイッククイズ4.19
でも、1000命令は、クリティカルセクションとして、特別に小さいとは言えません。ずっと小さなクリティカルセクション、例えば、数十命令しか持たないようなものが必要な時には、どうしたらいいでしょう?
クイッククイズ4.20
図4.10で、100M 以外の線の全ては、理想的な線から大きく外れています。それに対して、100M の線は、64CPUで、理想的な線から鋭利に落ちます。さらに、100M の線と 10M の線の間隔は、10M の線と 1M の線の間隔に比べてずっと小さいです。100M の線は、なぜ、他の線と比べてこんなに異なる振る舞いをするのでしょう?
クイッククイズ4.21
Power-5 は何年も前に出たものです。新しいハードウェアはより速いはずです。ならば、リーダーライターロックが遅いことなど、誰も気にする必要はないのではないでしょうか?
これらの制限がありますが、リーダーライターロックは多くの場合に、とても便利です。例えば、リーダーが高遅延のファイルやネットワークI/Oをしないといけない時など。他の手段もあります。5章と9章でそのいくつかを紹介します。
4.3 アトミック操作
図4.10によると、クリティカルセクションが最も小さいときに、リーダーライターロックのオーバーヘッドは最もひどいですから、最も小さいクリティカルセクションを守るための他の方法があるとありがたいです。アトミック操作はそのような方法の一つです。私達は既に、一つのアトミック操作を見ています。図4.9の18行目の __sync_fetch_and_add() プリミティブです。このプリミティブは、その二番目の引数の値を、一番目の引数が参照する値にアトミックに加算します。そして古い値(今の場合、無視されます)を返します。2つのスレッドが同時に同じ変数に対して __sync_fetch_and_add() を実行したら、その変数の結果となる値は、両方の加算の結果を含みます。 gcc コンパイラは、それ以外にも多くのアトミック操作を提供します。それらは、__sync_fetch_and_sub(), __sync_fetch_and_or(), __sync_fetch_and_and(), __sync_fetch_and_xor(), そして __sync_fetch_and_nand(), これらは皆、古い値を返します。あなたがその代わりに、新しい値がほしいなら、 __sync_add_and_fetch(), __sync_sub_and_fetch(), __sync_or_and_fetch(), __sync_and_and_fetch(), __sync_xor_and_fetch(), そして __sync_nand_and_fetch() プリミティブを使って下さい。
クイッククイズ4.22 両方のプリミティブのセットを持つことが本当に必要なのですか?
古典的な、コンペアアンドスワップ操作は、2つのプリミティブで提供されます。 __sync_bool_compare_and_swap() と __sync_val_compare_and_swap() です。これらのプリミティブは両方とも、ある位置を新しい値にアトミックに更新します。ただし、その前の値が、指定された古い値に等しい時だけです。最初の変種は操作が成功したら1,失敗したら0を返します。失敗とは例えば、前の値が、指定された古い値と違う時です。2つ目の変種は、その位置の元の値を返します。それが指定された古い値と等しいならば操作が成功したことを示します。このコンペアアンドスワップ操作はどちらも「ユニバーサル」です。つまり、単一の位置への任意のアトミック操作は、コンペアアンドスワップを使って実装できるという意味です。ただ、最初に述べた操作は、それらが使えるところではしばしばより効率的です。コンペアアンドスワップ操作は、より広いアトミック操作のセットの基礎として働くこともできます。ただ、それらのうちより洗練されたものは、しばしば複雑で、スケーラビリティと性能の問題に苦しむことがあります [Her90]。
__sync_synchronize() プリミティブは「メモリバリア」を発行します。それは、コンパイラとCPUが操作をリオーダーする能力をどちらも制限します。詳しくは14.2節で議論します。コンパイラが操作をリオーダーする能力だけを制限し、CPUは自由にしておくのが十分な場合もあります。その時には、barrier() プリミティブを使うことができます。実はこれは、図4.9の28行目にありました。コンパイラがあるメモリアクセスを最適化の結果、無くしてしまうのを避けるだけで良い場合もあります。その時は、ACCESS_ONCE() を使うことができます。これは、図4.6の17行目にありました。最後の二つのプリミティブは gcc で直接提供されませんが、以下のように直裁的に実装することができます。
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
#define barrier() __asm__ __volatile__("": : :"memory")
クイッククイズ4.23 これらのアトミック操作はしばしば、元となる命令セットによって直接サポートされている単一のアトミック命令を生成することができます。そう考えると、それらの命令は、ものごとを行うための考えうる最速の方法なのではないですか?
4.4 Linux カーネルでのPOSIX操作の同等品
不幸にも、スレッド操作、ロックプリミティブ、そしてアトミック操作は多くの標準化委員会がそれらのまわりにたどり着くはるか以前からかなり広く使われていました。この結果、これらの操作がどのようにサポートされているかに関しては、かなりのバリエーションがあります。これらの操作が今でもアセンブラ言語で実装されているのを見るのはとても一般的です。一つには歴史的理由から、あるいは特別な状況でより良い性能を得るためです。例えば、 gcc の __sync__ プリミティブのファミリーはすべて、メモリオーダリングセマンティクスを提供します。多くの開発者はこれを使って、メモリオーダリングセマンティクスが必要ない状況を作るために、自分自身の実装を作ることができます。 なので、47ページの表4.1は、POSIXと gcc プリミティブを、Linuxカーネルで使われているものと大まかにマップしたものです。正確なマッピングが常にあるわけではありません。例えば、Linuxカーネルは広い種類のロックプリミティブを持っていますが、gcc は、Linuxカーネルでは直接使うことができないいくつかのアトミック操作を持ちます。もちろん、一方ではユーザレベルのコードは、Linuxカーネルの豊富なロックプリミティブは必要ありません。また一方では、gcc のアトミック操作は、cmpxchg() を使って、十分直裁的にエミュレートできます。
クイッククイズ4.24 fork() と wait() の、Linuxカーネルの同等品はどうなったのですか?
4.5 仕事のための正しい道具: どのように選ぶか?
大まかな規則を言えば、仕事を片付けることのできる最も単純な道具を使いましょう。可能なら、ただ、シーケンシャルにプログラムしなさい。それが十分でないなら、並列性を仲介するためのシェルスクリプトを使ってみましょう。その結果となるシェルスクリプトの fork()/exec() オーバヘッド(Intel Core Duo ラップトップで最小のCプログラムを動かして約480マイクロ秒)が大きすぎるなら、C 言語の fork(), wait() プリミティブを使ってみましょう。これらのプリミティブのオーバーヘッド(最小の子プロセスに対して約80マイクロ秒)がそれでも大きすぎるなら、POSIX スレッドプリミティブを使い、適切なロックそして/あるいはアトミック操作プリミティブを選ぶ必要があるかもしれません。POSIX スレッドプリミティブのオーバーヘッド(典型的にはマイクロ秒以下)が大きすぎるなら、9章で紹介するプリミティブが必要かもしれません。プロセス間通信とメッセージパッシングは、共用メモリマルチスレッド実行に対する良い代替策でありうることを常に思い出してください。 クイッククイズ4.25 シェルは通常は、 fork() でなくて vfork() を使うのでないですか? もちろん、実際のオーバーヘッドはあなたのハードウェアに依存するだけでなく、あなたがそのプリミティブをどのように使うかに致命的に依存します。なので、それぞれのプリミティブを正しく選ぶだけでなく、正しい設計上の選択をすることも必要です。それについては、以降の章で十分に議論します。
以上