C++でSingletonを実装する場合について考えて見ましょう。
ざっと思いつくだけで3通りくらいの実装方法があります。
(1) クラススコープのstatic変数とする場合
(2) 関数スコープのstatic変数とする場合
(3) new してヒープにつくる場合
やっかいなことに、どの実装方法がよいかはSingletonの使われ方によります。
Loggingというログ収集クラスを例にサンプルを作ってみましょう。
(1)の場合の実装はこんな感じでしょうか…
class Logging
{
public:
static Logging& Instance();
...
void trace(...);
...
private:
Logging(); // Singletonにお決まりのコンストラクタ隠蔽
~Logging() {} // 勝手にデストラクタを呼ばれないように…
Logging(Logging&); // コピーコンストラクタも隠蔽すべし!
void operator = (Logging&);
static Logging singleton; // クラススコープのstatic変数にする場合
...
};
Logging Logging::singleton;
Logging& Logging::Instance()
{
return singleton;
}
...
この場合、構築はプロセス起動時(ダイナミックリンクライブラリならライブラリロード時)におこなわれます。したがって、構築に関してはスレッドの衝突を考えなくてよいというメリットがあります。しかし、デメリットもあります。構築のタイミングをプログラマーが制御できないのです。別のコンパイル単位でつくられた他のSingletonやstatic変数のコンストラクタ内で上のsingletonが使われるとき、singletonの構築が完了していることを保障できないのです。
(2)の実装を見てみましょう…
class Logging
{
public:
static Logging& Instance();
...
void trace(...);
...
private:
Logging(); // Singletonにお決まりのコンストラクタ隠蔽
~Logging() {} // 勝手にデストラクタを呼ばれないように…
Logging(Logging&); // コピーコンストラクタも隠蔽すべし!
void operator = (Logging&);
...
};
Logging& Logging::Instance()
{
static Logging singleton; // この関数がはじめて呼ばれたときに構築される
return singleton;
}
...
この実装は初めてsingletonが使われるときに構築がおこなわれるため、他のstatic変数のコンストラクタから使用されても確実に構築が完了したsingletonを返すことができ、(1)のデメリットを克服しています。しかし、スレッドセーフティに関しては微妙です… 昔、VC++ 6.0で試したのですが、コンストラクタの中にSleep()をいれてわざと処理に時間がかかるコンストラクタをつくり、複数のスレッドからほぼ同時にInstance()関数を呼ばせたところ、いちばん最初にInstance()関数を呼んだスレッドのコンテキスト でコンストラクタが実行されたものの、他のスレッドはコンストラクタの処理が完了していないsingletonを受け取ってしまっていました。スレッドセーフにするためにミューテックス を使えばよいと思うかもしれませんが、ミューテックスの構築がいつおこなわれるのか考えると同じジレンマに陥ってしまいそうです…
(3)の実装はGoF 本にも書かれているやり方です…
class Logging
{
public:
static Logging& Instance();
...
void trace(...);
...
private:
Logging(); // Singletonにお決まりのコンストラクタ隠蔽
~Logging() {} // 勝手にデストラクタを呼ばれないように…
Logging(Logging&); // コピーコンストラクタも隠蔽すべし!
void operator = (Logging&);
static Logging* singleton; // singletonを指すポインタ
...
};
Logging* Logging::singleton = 0;
Logging& Logging::Instance()
{
if (0 == singleton)
{
singleton = new Logging();
}
return *singleton;
}
...
典型的なLazy Initializationですが、当然、このままではスレッドセーフではありません… スレッドセーフにするためには排他制御が必要です。パフォーマンスを考慮してDouble Checked Locking Pattern にするとこんな実装になるでしょうか…
Logging& Logging::Instance()
{
if (0 == singleton)
{
mutex.lock(); // クリティカルセクション
if (0 == singleton)
{
singleton = new Logging();
}
mutex.unlock();
}
return *singleton;
}
ここで、mutex.lock/unlockの部分はWindowsだとEnterCriticalSection/LeaveCriticalSection(MFCならCCriticalSection::Lock/Unlock)、Linuxならpthread_mutex_lock/pthread_mutex_unlockなどです。スレッドセーフにするとこの関数が呼ばれたときには確実にミューテックスが構築されていなければならないため、(1)と同様に他のstatic変数のコンストラクタからは使えないでしょう…
マルチスレッドに対応する必要がなければ、(2)か(3)のやり方がいいでしょう。しかし、構築の部分がスレッドセーフでなければならない、かつ、他のSingletonやstatic変数のコンストラクタからも使用される可能性を考慮しなければならないとなると完璧な実装は難しいです…
完璧な実装が無理ならば、Singletonの使用方法を限定するしかありません。たとえば、スレッドの衝突をさけて構築は必ずメインスレッドでやるようにするとか、あるいは、static変数になる可能性のあるオブジェクトのコンストラクタではSingletonを使用してはいけないなどなど…
もし、関数ローカルなstatic変数の構築中は他のスレッドをブロッキングしてくれるようにコンパイラがうまくやってくれるならば、構築に関しては(2)の実装がベストになるでしょう。しかし、そこまでやってくれるコンパイラが存在するのか?… GCCの-fthreadsafe-staticsオプションが使える場合がそうです(逆にシングルスレッドの場合は-fno-threadsafe-staticsオプションをつけてコンパイルするとよいかもしれません)。
次回はSingletonの破棄について考察してみましょう。
追記:
mutexの初期化方法についていい方法を思いつきました♪
詳しくはこちらの記事 を参考にしてください。