2007年08月15日
並列プログラミング(その2)
3.Memory Ordering
シングルプロセッサのマルチスレッドでは、volatile変数をフラグにして簡単な同期を書くことができました。 例えば、次のような感じです。(コンパイラはvolatile変数へのアクセスの順序を入れ替えないものとします)
volatile int done = 0; volatile struct { int foo; int bar; } foobar; void writer(void) { foobar.foo = fizz(); foobar.bar = bazz(); done = 1; } void reader(void) { int foo, bar; while (!done) sleep(1); foo = foobar.foo; bar = foobar.bar; }
これは、マルチプロセッサ環境では上手くいかないことがあります。今時のCPUは、命令を順番に実行するとは限らないからです。例えば、メモリ書き込みを後回しにしたり、メモリ読み込みを投機的に(先走って)実行します。
こういった順番の入れ替えは、そのプロセッサ単体で見たときにはプログラムの実行に影響が無いようにされていますが、他のプロセッサから見たときにはメモリの更新順序が異なって見えてしまいます。
この問題を解決するために、メモリフェンスという仕組みがあります。例えば、i686では lfence, sfence, mfence という命令があります。lfence命令をプログラムにはさむと、lfence命令より後ろのLoad命令が、lfence命令より先に実行されることがなくなります。sfence命令はlfence命令の逆で、sfence命令より前のStore操作が完了する(キャッシュメモリなどに書き込まれる)のを待ちます。mfence命令はlfenceとsfenceを足したものです。
先ほどのプログラムを、マルチプロセッサでも動くようにメモリフェンスを挿入すると、次のようになります。
volatile int done = 0; volatile struct { int foo; int bar; } foobar; void writer(void) { foobar.foo = fizz(); foobar.bar = bazz(); asm(" sfence;"); // fooとbarの書き込みが確実に実行されるのを待つ. done = 1; } void reader(void) { int foo, bar; while (!done) sleep(1); asm(" lfence;"); // doneが真になる前に、fooやbarをLoadされるのを防ぐ. foo = foobar.foo; bar = foobar.bar; }
もちろん、自分で同期しようとしないで同期APIを呼べば、ちゃんとメモリフェンスもしてくれます。 この領域の問題はデバッガで追えるようなモノではなく、非常にデバッグが難しいので、最初から同期APIを 使用することをお勧めします。