例 9-5 は、「生産者 / 消費者」問題の代表的な解決方法です。
このプログラムは、現在の SPARC ベースのマルチプロセッサでは正しく動作しますが、すべてのマルチプロセッサが強く順序付けられたメモリーをもつことを想定しています。したがって、このプログラムには移植性がありません。
char buffer[BSIZE]; unsigned int in = 0; unsigned int out = 0; void char producer(char item) { consumer(void) { char item; do ;/* 処理なし */ do while ;/* 処理なし */ (in - out == BSIZE); while (in - out == 0); buffer[in%BSIZE] = item; item = buffer[out%BSIZE]; in++; out++; } } |
このプログラムは、生産者と消費者がそれぞれ 1 つしか存在せず、かつ共有メモリー型のマルチプロセッサ上で動作するときは正しく動作します。in と out の差が、バッファ内のデータ数となります。
生産者はバッファに空きができるまで、この差を繰り返し計算しながら待ちます。消費者は、バッファにデータが入れられるのを待ちます。
強く順序付けられたメモリー (たとえば、あるプロセッサのメモリーへの変更が他のプロセッサにただちに伝わるようなメモリー) では、この方法は成立します (BSIZEが 1 ワードで表現できる最大整数より小さい限り、in と out が最終的にオーバーフローしても成立します) 。
共有メモリー型のプロセッサは、必ずしも強く順序付けられたメモリーをもつ必要はありません。つまり、あるプロセッサによるメモリーへの変更が、他のプロセッサにただちに伝わるとは限りません。あるプロセッサによって、メモリーに 2 つの変更が加えられた場合、メモリーの変更がただちに伝わらないので、他のプロセッサから検出できる変更の順序は最初の順序と同じであるとは限りません。
変更内容は、まず「ストアバッファ」に入れられます。このストアバッファは、キャッシュからは参照できません。
プロセッサは、データの整合性を保証するためにストアバッファをチェックします。しかし他のプロセッサから、このストアバッファは参照できません。つまり、あるプロセッサが書き込んだ内容は、キャッシュに書き込まれるまで他のプロセッサから参照できません。
同期プリミティブ (第 4 章「同期オブジェクトを使ったプログラミング」「同時オブジェクトを使ったプログラミング」を参照) は、特別な命令でストアバッファの内容をキャッシュにフラッシュしています。したがって、共有データをロックで保護すればメモリーの整合性が保証されます。
メモリーの順序付けが非常に弱い場合は、例 9-5では問題が生じます。消費者は、生産者によって in が 1 つ増やされたことを、対応するバッファスロットへの変更を知る前に知る場合があるからです。
あるプロセッサのストア順序が、別のプロセッサからは違った順序で見えることがあるため、これを「弱い順序付け」と呼びます (ただし、同じプロセッサから見たメモリーは常に整合性を保っています)。この問題を解決するには、相互排他ロックを使用して、ストアバッファの内容をキャッシュにフラッシュしなければなりません。
最近は、メモリーの順序付けが弱くされる傾向にあります。このため、プログラマは広域データや共有データをロックで保護することに一層注意してください。