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の件と例外の件は解決しました。しかし、上記は完全ではありません。いくつか問題があります。
- 素の pthread_mutex_t を使用するのはC++的ではない。特に下記の点が問題:
- 他のMutex型と同一に扱えない
- 他のMutex型と同一のScopedLockクラスを用いてロックできない
- 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 の意