以下は、perfbook の付録Aの kanda.motohiro@gmail.com による全訳です。編集に不便なので、ファイルを分けました。perfbook の訳の他の部分は、親文書を参照。
付録A 重要な質問
以下の節は、SMPプログラミングに関するいくつかの重要な問題を議論します。それぞれの節には、その質問に悩まなくてもいいための方法も書いてあります。それは、あなたのゴールが単純に、あなたのSMPコードをできる限り早く、苦痛なく動くようにすることであるならば、とても重要でしょう。それにしても、それは素敵なゴールですね!
以下の質問への答えは、シングルスレッドの環境の場合と比べると、しばしばかなり直感的ではありませんが、少し努力すれば、理解するのはそれほど難しくはありません。もしあなたが、繰り返しをいとわないなら、ここには圧倒するほどの困難を強いるものはありません。
A.1 「後」とはどういう意味でしょう?
「後」は、直感的ですが、驚くほど複雑な概念です。直感に反する重要な問題は、コードはどこにおいても、任意の時間、遅延することがあることです。タイムスタンプ "t"、整数フィールド "a", "b", "c" を持つグローバル構造体を使って通信する、生産者と消費者のスレッドを考えましょう。生産者は図A.1に示すように、現在時刻(1970年からの秒数を十進数で示したもの)を記録し、そして "a", "b", "c" の値を更新しながらループします。消費者のコードは、図A.2に示すように、同様に現在時刻を記録しますがこちらは生産者のタイムスタンプをフィールド "a", "b", "c" とともに複写します。実行の終わりに、消費者は異常な記録、つまり、時間が逆に進んだように見えるものの一覧を出力します。
クイッククイズA.1
この例で、どのようなSMPコーディング誤りを見つけられますか?完全なコードは、 time.c を参照下さい。
直感的には、生産者と消費者のタイムスタンプの差はとても小さいと期待するかもしれません。生産者がタイムスタンプや値を記録するのにそんなに時間がかかるはずはないでしょうから。表1に、デュアルコア 1GHz x86 で取ったサンプル出力の一部を示します。ここで、“seq” カラムは、ループを回った回数です。“time”カラムは、異常が起きた時刻を秒で示したものです。“delta”カラムは、消費者のタイムスタンプが生産者のそれの何秒後かを示したものです(負数は消費者が生産者よりも早くタイムスタンプを得たことを示します)。そして、“a”, “b”, と “c”と書いてあるカラムは消費者の前回のスナップショット採取以来、これらの変数がどれだけ増したかを示します。
なぜ、時間は逆に進むのでしょう?かっこの中の数字は、マイクロ秒を単位とする差です。10マイクロ秒を越える大きな値があり、一つは何と100マイクロ秒を超えます!このCPUは潜在的にその時間の間に10万命令以上を実行する能力があることに注意下さい。
以下のイベントのシーケンスは一つのあり得る理由です。
1. 消費者はタイムスタンプを得ます。(図A.2 の13行目)
2. 消費者はプリエンプトされます。
3. 任意の長さの時間が過ぎます。
4. 生産者はタイムスタンプを得ます。(図A.1 の10行目)
5. 消費者が再び動き出し、生産者のタイムスタンプをピックアップします。(図A.2 の14行目)
このシナリオだと、生産者のタイムスタンプは消費者のタイムスタンプよりも任意の長さの時間、後になることがあります。
あなたのSMPコードで、「後」の意味のために苦しむのを避けるにはどうしたらいいでしょう?
単純に、SMPプリミティブを設計された通りに使いなさい。
この例では、最も簡単な修正はロックを使うことです。例えば、生産者において、図A.1 の10行目の前にロックを取ります。消費者において、図A.2 の13行目の前にロックを取ります。このロックは、図A.1の13行目の後と、図A.2の17行目の後に放さないといけません。このロックは、図A.1の10から13行目と、図A.2の13から17行目のコード断片がお互いに排他するようにします。言葉を代えて言うと、お互いに、アトミックに走るということです。図A.3にこれを示します。ロックは、コードの箱のどれもが、時間の上でオーバーラップするのを防ぎます。なので、消費者のタイムスタンプはその前の生産者のタイムスタンプの後に取得されます。この図でそれぞれの箱の中のコード断片は「クリティカルセクション」と呼ばれます。ある時点では、そのようなクリティカルセクションの一つだけが実行することができます。
ロックを追加すると結果は、表A.2に示した出力となります。ここでは、時間が逆に進んだ例はありません。その代わり、消費者が続けてリードをした時に1000カウント以上の差がある場合だけがあります。
訳注。原文は図だけど、表。
クイッククイズA.2
消費者が続けてリードをした時にこんなに大きなギャップがあるのはなぜでしょう?完全なコードは timelocked.c を参照下さい。
まとめると、あなたが排他ロックを取ったなら、あなたがそのロックを持ったままする全ての事は、そのロックを以前に持っていた全ての人がした全ての事の後に起きたように見えることをあなたは信じることができます。どのCPUがメモリバリアを実行したかしなかったのかを心配する必要はありません。CPUあるいはコンパイラが操作をリオーダーするかを心配する必要はありません。人生は単純です。もちろん、このロックが、これらの2つのコード部分が同時に実行しないようにするという事実は、プログラムがマルチプロセッサにおいてより大きな性能を達成する能力を制限するかもしれません。それは、「安全だけど遅い」状況になるかもしれません。6章は、多くの状況で、性能とスケーラビリティを得る方法をいくつか述べます。
しかし、ほとんどの場合、もしあなたが、あるコード部分の前、あるいは後に、何が起きるかを心配することがあれば、あなたはそれを、標準のプリミティブをより良く使うためのヒントとしてとらえるべきです。プリミティブに、あなたに代わって心配をしてもらいましょう。
A.2 今何時ですか?
図A.4は、マルチコア計算機システムでタイムキーピングをする時に鍵となる問題点を示します。一つの問題は、時刻を読むのに時間がかかることです。命令がハードウェアクロックを読むとします。このリード操作を完了するためには、コアを離れて(もっと悪いことにソケットを離れて)行かないとだめかもしれません。さらに、読んだ値に対して、なんらかの計算をする必要があるかもしれません。例えば、望む形式にするとか、ネットワーク時刻プロトコル(NTP)の補正を適用するなど。なので、最終的に返される時刻は、結果となる時間間隔の最初に対応するのか、最後なのか、途中のどこかなのか、どれでしょう?
更に悪いことに、時刻を読むスレッドは割りこまれたりプリエンプトされたりするかもしれません。またまた、時刻を読んだ時と、実際に読んだ時刻を使う時の間に、何らかの計算があるというのはよくあるでしょう。これら可能性のどちらもが、さらに不確定の間隔を広げます。
一つのアプローチは、時刻を二回読むことです。そして、2つの結果の算術的平均を取ります。タイムスタンプされる操作の両側の端で一度ずつ、が良いでしょうか。2つの結果の差は、そうすると、介入操作が起きた時の時刻の不確定の度合いと考えることができます。
もちろん多くの場合、正確な時刻は不要です。例えば、人間の利用者の利便のために時刻を表示する場合、人間の反射神経は遅いですから、ハードウェアとソフトウェアの内部的な遅延はどうでも良くなります。同様に、サーバがクライアントへの応答をタイムスタンプする必要がある時、要求を受け取った時から、応答を送った時の間にある時間ならば、いつでも同じくらい役に立つでしょう。
以上