C++0x時代の Double-Checked Locking

今回は "Double-Checked Locking" (以下DCL)というマルチスレッドプログラム向けのイディオムを例にして、C++0xの(低レイヤ向け)マルチスレッド機能の利用方法を紹介してみます。
DCLとは、「ロック→条件判定」というロジックを「条件判定→ロック→(再度)条件判定」と書き換えるイディオムで、主に遅延初期化などの処理においてロックのオーバーヘッドを減らすために用いられます。DCLはシンプルかつ効果の高いイディオムだったので、一時期もてはやされました。ところが、DCLはコンパイラやCPUによるリオーダーの影響により正しく動作しない場合があることがわかったため(参考1参考2)、今ではアンチパターンと呼ばれることすらある始末です。
しかし、DCLの問題点は、メモリモデルに関する知識があまり知られていなかったことと、プログラミング言語の仕様でメモリモデルが正しく定義されていなかったことにあり、DCLというイディオムそのものは正当なものです。そして、atomic変数とメモリバリアがきちんと定義されている C++0x では、DCLを正しく実装することが可能になったのです。
ではさっそく、「正しい」DCLの実装例を挙げてみましょう。

std::atomic p(nullptr);
std::mutex m;

Hoge& getInstance() {
  Hoge* tmp = p;
  if (tmp == nullptr) {
    std::lock_guard lk(m);
    tmp = p;
    if (tmp == nullptr) {
      tmp = new Hoge();
      p = tmp;
    }
  }
  return *tmp;
}

std::mutex や std::lock_guard は C++0x で追加されたロックを行うためのAPIです。上のコードだと「"std::lock_guard lk(m);" の行で排他ロックを行い、変数lkのスコープの終わり(つまりreturn文の前の行)でアンロックする」という意味になります。DCLの特徴である「条件判定→ロック→(再度)条件判定」という実装になっていることがわかるでしょうか。
このDCL実装の肝は、変数 p がatomic型として宣言されている点です。 C++0x のatomic型は、これまで繰り返し説明してきたように、操作がアトミック(不可分)に行われるだけでなく、メモリアクセスの順序付けを保証する効果(メモリバリア)も持っています。変数 p への読み書きの際にメモリバリア効果が発揮されるため、最初に挙げた「DCLの問題点」を解消できているのです。

続きを読む