このページはC++11に採用された言語機能の変更を解説しています。
のちのC++規格でさらに変更される場合があるため関連項目を参照してください。
概要
ブロックスコープを持つstatic
変数の初期化は、スレッドセーフであることが規定された。
static
変数の初期化が完了するまで、他のスレッドは初期化処理の前で待機する。
class singleton {
public:
static singleton& get_instance()
{
static singleton instance; // この初期化はスレッドセーフ
return instance;
}
};
仕様
staticローカル変数の初期化
- ローカルの
static
変数を宣言と同時に初期化した場合、並行実行は初期化が完了するまで待機しなければならない
非ローカルなstatic変数の初期化
- 非ローカルな
static
変数の初期化は、main()
関数が開始する前に順不同で行われること
static変数のデストラクタ
static
変数のデストラクタは、全てのスレッドが終了したあとに、宣言と逆の順番にデストラクタが呼び出され、破棄される
例
#include <thread>
#include <vector>
#include <cassert>
class singleton {
int value_ = 3;
public:
static singleton& get_instance()
{
static singleton instance; // この初期化はスレッドセーフ
return instance;
}
int get() const
{ return value_; }
};
int main()
{
// 4スレッドで並行に、
// singletonのstatic変数へとアクセスする
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
std::thread t([] {
// singletonのメンバ変数value_が
// 常に初期化された状態であることを確認
assert(singleton::get_instance().get() == 3);
});
threads.push_back(std::move(t));
}
for (std::thread& t : threads) {
t.join();
}
}
出力
この機能が必要になった背景・経緯
ローカルのstatic
変数を初期化することがスレッドセーフではなかった前バージョンまでは、同じことをするために、Double Checked Lockingという技法によって、ユーザーが初期化をスレッドセーフにしていた。それは、以下のようなものである:
static
ローカル変数の代わりに、ポインタをstatic
メンバ変数として持つ- ポインタがヌルかどうかをチェックし、ヌルであればロックを取得する
- ヌルチェックとロック取得の間に有効なポインタになる可能性があるためにロック取得後にさらにヌルチェックをし、ヌルであれば初期化をする
こういった技法は非常に難しく、潜在的な並行性バグの可能性があるためにユーザーの行動を制限していた。
そのために、言語によってstatic
変数の初期化がスレッドセーフであることを保証することとなった。
参照
- N2148 Dynamic Initialization and Destruction with Concurrency
- N2325 Dynamic Initialization and Destruction with Concurrency
- N2382 Dynamic Initialization and Destruction with Concurrency
- N2444 Dynamic Initialization and Destruction with Concurrency
- N2513 Dynamic Initialization and Destruction with Concurrency
- N2660 Dynamic Initialization and Destruction with Concurrency
- C++0x時代の Double-Checked Locking - yamasaのネタ帳