C++でsynchronized methodを書くのは難しい (1)

Javaにはsynchronizedという便利なキーワードがあります。このキーワードを使うと、例えば次のように簡単にメソッドを「同期化」することができます。同期化されたメソッドは、複数のスレッドで同時に実行されることがありません。

public class Foo {
  ...
  public synchronized boolean getFoo() { ... }

さて、C++ (with pthread) で同様の機能を実現するにはどうしたらよいでしょう?まず、一番単純な方法は次のようなものです。

// 方法 a

void Foo::need_to_sync(void) {
  static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  pthread_mutex_lock(&mutex);

  // 処理

  pthread_mutex_unlock(&mutex);
  return;
}

この方法は、C言語の場合はともかく、C++言語で用いるには若干問題があります。それは

  • 「処理」の途中でreturn
  • 「処理」の途中で例外が送出

された場合にmutexがunlockされないことです。この点を改良すると、コードは次のようになります。

// 方法 b

class ScopedLock : private boost::noncopyable {
public:
  explicit ScopedLock(pthread_mutex_t& m) : m_(m) {
    pthread_mutex_lock(&m_);
  }
  ~ScopedLock(pthread_mutex_t& m) {
    pthread_mutex_unlock(&m_);
  }
private:
  pthread_mutex_t& m_;
};

void Foo::need_to_sync(void) {
  static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  { // この括弧はなくてもいいですが。
    ScopedLock lock(mutex);

    // 処理

  }
  return;
}

OK。returnの件と例外の件は解決しました。しかし、上記は完全ではありません。いくつか問題があります。

  1. 素の pthread_mutex_t を使用するのはC++的ではない。特に下記の点が問題:
    • 他のMutex型と同一に扱えない
    • 他のMutex型と同一のScopedLockクラスを用いてロックできない
  2. Javaのsynchronizedメソッドは「再帰ロック可能」であるが、上記コードはそうなっていない。「処理」が自メソッドを再帰呼び出しするとデッドロックしてしまう

特に2.の再帰ロックの問題が重要でしょう。これは、glibc拡張を用いて良いなら次のように解決できます。

// 方法 c

void Foo::need_to_sync(void) {
  static pthread_mutex_t mutex = PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP;

NP*1というサフィックスからわかるように、この方法は移植性がありません。再帰mutexを初期化するには、pthread_mutex_init関数を用いなければなりません。pthread_mutex_init関数は、「一つのスレッドが一回だけ」呼ばなければなりません。これをsynchronized method的手法で実現しようとすると「鶏が先か卵が先か」という話になってしまいますので、pthread_onceという関数を用いて実装するのがSUSv3にも載っている定石です。

// 方法 d

namespace /* anonymous */ {
  pthread_once_t      once = PTHREAD_ONCE_INIT;
  pthread_mutex_t     mutex;
  pthread_mutexattr_t attr;

  void mutex_init() {
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&mutex, &attr);
  }
}

void Foo::need_to_sync(void) {
  pthread_once(&once, mutex_init);
  {
    ScopedLock lock(mutex);

    // 処理

  }
  return;
}

OK。これで再帰ロックの問題は解決できました。しかし…。この方法は

  • 益々C++的ではない。synchronizeしたいメソッド毎にこんな処理を記述するのは非効率すぎる
  • ランタイムコストが大きい。遅い。

という新たな問題を生んでしまいます。


続き

*1:non portable の意

C++でsynchronized methodを書くのは難しい (2)

「いや、メソッドの同期化など頻繁に必要になるはずだ、便利な方法がないわけがない」と仰る方もいるでしょう。はい、あります。一般的な方法は、

  • (non-POD型の、つまり普通の)C++のクラスとして「Mutexクラス」を用意する
  • クラス変数、あるいはグローバル変数としてMutexクラスのインスタンスを用意し、それを用いてメソッドを同期化する

となります。


具体的に見ていきましょう。まず、Mutexクラスというのは次のようなものです*1

class Mutex {
public:
  Mutex() {
    pthread_mutex_init(&m_, 0);
  }
  void Lock(void) {
    pthread_mutex_lock(&m_);
  }
  void Unlock(void) {
    pthread_mutex_unlock(&m_);
  }
private:
  pthread_mutex_t m_;
};

世の中のMutexクラスは、抽象基底クラス(インタフェースクラス)を持っている場合も多いと思いますが、ここでは省略します。ScopedLockクラスも若干修正が必要ですね。次のようにしておけば良いでしょう。

template<typename T> class ScopedLock {
public:
  ScopedLock(T& m) : _m(m) {
    _m.Lock();
  }
  ~ScopedLock() {
    _m.Unlock();
  }
private:
  T& _m;
};

このMutexクラスを用いてメソッドを同期するには、次のようにします。まずは明らかに間違った例から。

// 方法e

void Foo::need_to_sync(void) {
  static Mutex mutex;
  {
    ScopedLock<Mutex> lock(mutex);

    // 処理

  }
  return;
}

これは…コードは簡単で良いのですが、残念ながらきちんと動作しません。NGです。Foo::need_to_sync関数の初回実行が偶然、複数のスレッドによって同時に行われると、mutexのコンストラクタが複数回呼ばれる可能性があります。理由については、MSの中の人のblogとして名高い The Old New Thing の記事、"C++ scoped static initialization is not thread-safe, on purpose!"などで熟知していただくのが良いと思いますので詳しくは述べません*2。このblogではVC++を例にしていますが、g++でも話は同じで、「局所的静的変数の動的初期化処理」は、スレッドのことなど全く意識されずに行われます*3


次に、世間でよく行われている方法を紹介します。簡単のためグローバル変数にしておきますが、クラス変数(クラスのstaticなメンバ変数)としても同じことです。「非局所的な静的変数」としてMutexを用意する方法です。

// 方法f

namespace /* anonymous */ {
  Mutex mutex;
}

void Foo::need_to_sync(void) {
  ScopedLock<Mutex> lock(mutex);

  // 処理

  return;
}

この方法は最もポピュラーと思われます。しかも、たいてい問題なく動きます。

問題が起きるのは、別のグローバルなオブジェクトxがあり、xのコンストラクタから直接、あるいは廻りまわって間接的に、Foo::need_to_sync関数が呼ばれてしまう場合です。静的オブジェクトの初期化順の問題、一般に "static initialization order fiasco" などと呼ばれている問題です。mutexのコンストラクタが走る前に、mutex.Lock()が実行されてしまう可能性があるのです。


こちらのFAQの10.12〜10.16を読み*4、その上で自分のコードに初期化順の問題がなく、将来も問題が起こらないと確信できるのでしたら上記方法を使用してOKと思います。


もし、初期化順の問題が起きないと確信できないのなら、pthread_onceを用いた「方法d」を使用するか、やや移植性の低い「方法c」を使用するしかありません。個人的には割り切って方法cを使えば良いのではないかと思っています。


さて、最後に方法cを、もう少しC++的にする方法を考えてみます。

// 方法c (再掲)

void Foo::need_to_sync(void) {
  static pthread_mutex_t mutex = PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP;

目標は、

  • pthread_mutex_t 型を隠蔽し、自作したクラス型だけを見せる
  • 方法e,fで使用したScopedLockクラステンプレートをそのまま使用できるようにする

です。もちろん、初期化順の問題を再発させてはなりません。


続き

*1:再帰mutexにする例は冗長なので略します。pthread_mutex_initによる初期化を行っていますので、再帰mutex化するのは容易です。

*2:そのうちg++ -S結果とその解説でも載せます

*3:2005/12追記: 最近のg++では例外アリ、http://d.hatena.ne.jp/yupo5656/20051215/p2 を参照のこと

*4:和書 ASIN:489471194X に邦訳が載っています。また static initialization order の問題については此処にもそのうち何か書きます

C++でsynchronized methodを書くのは難しい (3)

方式cの改良です。まず、「初期化順」の問題を避けるためには、コンストラクタを呼び出すことなく初期化が済んでいるオブジェクトでなければなりません。そのためには、mutexオブジェクトを

// 方法c' (仮)

void Foo::need_to_sync(void) {
  static StaticMutex mutex = { PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP, ........ };

のように初期化できなければなりません。一般的なC++のクラスはこういう形の初期化は許されません。このような初期化を実現するためには、上記StaticMutexクラスはPOD型でなければなりません。POD型というのは、規格によると

  • コンストラクタがあってはダメ
  • デストラクタがあってはダメ
  • コピーコンストラクタ、代入演算子コンパイラが生成しないとダメ
  • private, protected なメンバがあってはダメ
  • 仮想関数があってはダメ

を満たす型のことです*1


随分厳しい制約ではありますが、「non-virtualなメンバ関数を定義するのは問題がない」などの性質を利用して、方式cを改良した方式c'を考案してみます。


...次で如何でしょう?

// 方式c'

#define POD_MUTEX_MAGIC 0xdeadbeef
#define STATIC_MUTEX_INITIALIZER           { PTHREAD_INITIALIZER,              POD_MUTEX_MAGIC }
#define STATIC_RECURSIVE_MUTEX_INITIALIZER { PTHREAD_RECURSIVE_INITIALIZER_NP, POD_MUTEX_MAGIC }

class PODMutex {
public:
  void Lock() {
    assert(magic_ == POD_MUTEX_MAGIC);
    pthread_mutex_lock(&mutex_);
  }
  void Unlock() {
    assert(magic_ == POD_MUTEX_MAGIC);
    pthread_mutex_unlock(&mutex_);
  }
  typedef ScopedLock<PODMutex> ScopedLock;

public:
  // POD型にするにはpublicにしないと駄目
  pthread_mutex_t mutex_;
  const unsigned int magic_;
};

// ScopedLockクラステンプレートは方法e,f用に作成したものを流用

void Foo::need_to_sync(void) {
  static PODMutex mutex = STATIC_RECURSIVE_MUTEX_INITIALIZER;
  {
    PODMutex::ScopedLock lock(mutex);

    // 処理

  }
  return;
}

pthread_mutex_t型を隠蔽する、ScopedLock<>を流用する、という先の目標を両方満たしています。少しはC++らしくなったのではないでしょうか。なお、PODMutex型は上記の例のような局所的静的変数以外にも、グローバル変数、クラス変数としても安心してお使いいただけます。


なお、メンバ変数 magic_ は、constなメンバ変数を置くことで、コンパイラによって自動生成されたコンストラクタによるオブジェクト生成をエラーにする意図で入れてありますが、リリースビルドからは取り除いたほうが良いでしょう。


上記のコードをg++ -Sでコンパイルし、アセンブリリストを吐かせてみると、局所的な静的変数 mutex は次のようになっています。

$ g++ -S sample.cpp
$ c++filt < sample.s | lv
(略)
        .size   Foo::need_to_sync()::mutex, 28
Foo::need_to_sync()::mutex:
        .long   0
        .long   0
        .long   0
        .long   1
        .long   0
        .long   0
        .long   -559038737

0,0,0,1,0,0 というのが PTHREAD_RECURSIVE_INITIALIZER_NP の部分、-559038737 が POD_MUTEX_MAGIC の部分です。動的初期化を行わずとも(コンストラクタを呼ばずとも)、オブジェクトファイル上に書かれた通りに静的初期化を行うだけでmutexオブジェクトが正常に初期化されることがわかります。OK。


ちなみにboostライブラリを使う場合は、方法f以外に選択の余地は(いまのところ)ないですね。MLなどを見ると、オーダー順の問題が出る可能性は(当然!!)理解しているが、方法c'相当のPOD-Mutexを移植性*2と速度を確保しつつ作成する方法がいまのところない、ということらしいです。


以上

*1:詳しくは ISO/IEC 14882:2003 あるいは JIS X 3014:2003 の「§3.9/10 C互換型」「§8.5.1/14 静的記憶期間をもつC互換型の集成体の波括弧で囲んだ初期化子並びによる静的な初期化」「§9/4 C互換構造体」を参照のこと

*2:Windowsはどうする?という命題を解決できないのかもしれません - 推測