マルチスレッドと共有変数 (続き) -- およびvolatile修飾の必要性

複数のスレッドで変数を共有し、さらにその変数に対してread/writeの両方のオペレーションが行われるとき、その変数の操作は、上で書いたとおり、

  • read/writeともにmutexで保護するべき
  • volatile修飾だけで済ませるのはNG
  • mutexで保護するならvolatile修飾は不要

です。その根拠を、規格を引きながら見てみましょう。


The Open Group Base Specifications Issue 6 の 4.10 Memory Synchronization には、

Applications shall ensure that access to any memory location by more than one thread of control (threads or processes) is restricted such that no thread of control can read or modify a memory location while another thread of control may be modifying it. Such access is restricted using functions that synchronize thread execution and also synchronize memory with respect to other threads. The following functions synchronize memory with respect to other threads:

(超訳) アプリケーションはあるスレッドが変更しているメモリを、別のスレッドが読んだり変更したりしてしまうことのないよう、同一メモリ領域へ2つ以上のスレッドによる同時アクセスが起きないことを保証すべきである。同時アクセスはスレッドの実行を同期化するような関数で防ぐことができ、そういう関数は別のスレッドに対してメモリの内容も同期する。
次の関数は別スレッドに対してメモリを同期する:

fork() pthread_barrier_wait() pthread_cond_broadcast() pthread_cond_signal() pthread_cond_timedwait() pthread_cond_wait() pthread_create() pthread_join() pthread_mutex_lock() pthread_mutex_unlock()
(以下略)

と書いてあります。pthread_mutex_lock()などの関数を使って同一メモリ領域への同時アクセスを防げば、メモリの同期が行われるんだそうです。まず、同時アクセスが起きないように保護しろと書いてあり、またvolatileを使えとは一言も書いていません。volatileを使いたい理由は、「あるスレッドAが、read-modify-writeした結果が実はレジスタに載ったままだった」という事態を危惧してのことだと思いますが、同時アクセスが起きないように保護すれば、「メモリ同期が行われる」と書いてあるのですからその現象は起こりません。


「いや、"Memory Synchronization" の定義が書いていないのでこれだけじゃなにもわからん」という方もおられると思いますが…書いていないのにも理由があります。§4.10 の Rationale*1に、

Formal definitions of the memory model were rejected as unreadable by the vast majority of programmers. (略) It simply states that the programmer has to ensure that modifications do not occur "simultaneously" with other access to a memory location.

とあるんですね。「普通のプログラマにはメモリモデルの形式的な定義を示しても理解されないから、"同時アクセスさえ防いでおけばすべてうまく行く"とだけ書いた」だそうです。ふーん*2POSIX準拠のシステムを作る側から見ると、「mutex(など)である変数への同時アクセスを防ぐだけで すべてがうまくいくように OS、ライブラリ、コンパイラ、ハードウェア が作られていないとPOSIX準拠にならない」ということです。


Rationaleでは"circular buffer"のコードを例に出し、メモリ同期をする関数を用いれば multi-processor 環境でもうまく動くと書かれていますので、先のレジスタの問題のみならず、マルチプロセサ環境での cache coherency の問題も、システム側で解決してもらえると見てよいでしょう*3


POSIX準拠を標榜するシステムを使うときは、read-modify-writeの有無にかかわらずきちんとmutexを使うよう心がけるとともに、mutexにすべてお任せして安心してvolatileを省略しましょう。


POSIXシステムが保証しなければならないこと(まとめ):

スレッドA,Bが同時に変数Xにアクセスしないようにコーディングされているのを前提とし、
あるスレッドAが変数Xの値をiへ更新してからmemory sync.をしたとき、
あるスレッドBがmemory sync.をしてから変数Xを読んだならiが得られる。


実装例:


上記の「保証しなけれなばらないこと」を保証するための典型的な方法*4としては、

1. memory sync. をするとされている関数群にメモリ操作のreorder防止のための細工をする
2. memory sync. をするとされている関数の実行前に共有変数の値をレジスタからメモリにフラッシュする

があり(1と2両方の実現が必要)、


1を実現するためには、「pthreadライブラリの当該関数の中に特別な命令*5を挿入しておくこと」が必要。2は普通のコンパイラは既に、「複数のスレッドから共有され得る変数と、そうでないものを見分けられる」かつ「上記に該当する変数は、全ての関数呼び出し前にメモリにフラッシュする」ように作られているため特に考慮不要。


といったところかと。


参考資料:
WikiPedia "memory barrier" (作成過程はここ)


続き

*1:規格策定までの論拠・背景の説明文書、厳密には規格外

*2:Javaですら、ちゃんとメモリモデルが示されているのになぁ…

*3:私は組み込み屋でMPに縁がありません。間違っていたら突っ込み願います

*4:とりあえずUPを仮定

*5:メモリバリアとなる命令