スレッド同期についてのエトセトラ


世の中には、volatile付けただけでその変数を同期プリミティブとして使用OKという誤った認識があるような気配だったので、ちょっと調べてみました。自分の認識を整理するためでもあるけど。

volatile修飾は何をしてくれるのか。まずはこれを考えてみます。
volatileはコンパイラによる最適化を抑制してくれます。これは、コンパイラによる命令の移動、メモリアクセスは高コストになるから変数をレジスタに割り当てる、といった部分が抑制されます。

この「レジスタに割り当てることをしない」という部分の話が、「常にメモリからデータを読み取る」と解釈されて広まった結果、マルチスレッド環境での同期プリミティブとして使用されてしまう、という状況のように感じます。

volatile変数で何とか同期プリミティブのようにしていた箇所は、アトミック変数+メモリバリア(メモリフェンス)の組み合わせで正しく作ることができます。メモリバリアは、CPUが命令を実行する際に起こる命令のリオーダーに関して働きます。簡単には、バリア命令の位置を超えてメモリ操作の命令を移動しない、という感じになります。

このメモリバリアも大別してacquireバリア/releaseバリアがあります.

  • アトミック変数への代入時には releaseバリアを併用します。
  • アトミック変数の読み取り操作時には、acquireバリアを併用します。
このバリアについてコード風に説明すると以下のようになります。

もう1つの例。

このように操作をまたがないようにする処理ということで、バリアと呼ばれます。

これを併用することにより、アトミック変数の値を取得した時点には、後続のメモリアクセス命令はまだ各種変数に代入されていない(評価されていない)ことが保証できます。
逆に、アトミック変数の更新時には、それまでのメモリ操作系の命令が、アトミック変数更新タイミングを超えて、後方で実行されるという点を防ぐことができます。

正しく同期化されたコードを書くためには、アトミック変数を使用するだけでなく、メモリバリア命令を併用する必要があります。

ちなみに、Mutex,Semaphoreなどの同期プリミティブはこれらのメモリバリアの内容を含んでいます。そのためこれらを使用してさえいれば、上記のやっかいな部分を意識しなくてすみます。

さてvolatile変数が同期プリミティブとして機能しない理由を考えてみます。
volatile変数はそもそも操作がアトミックではありません。
メモリからロード、演算、結果のストアというように加減算などは複数の命令の合成で処理されます。
また、先ほどのメモリバリアの効果も持たないので、前後のプログラムコードのメモリアクセスについて、アウトオブオーダーのCPUが実行順序を移動させることができてしまいます。これらの点からvolatile変数は同期プリミティブとして使用できないとなります。

話はまた次回へ続く。

スポンサーリンク

シェアする

  • このエントリーをはてなブックマークに追加

フォローする