最終更新日時(UTC):
が更新

履歴 編集

ブロックスコープを持つstatic変数初期化のスレッドセーフ化 [N2660](C++11)

このページは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という技法によって、ユーザーが初期化をスレッドセーフにしていた。それは、以下のようなものである:

  1. staticローカル変数の代わりに、ポインタをstaticメンバ変数として持つ
  2. ポインタがヌルかどうかをチェックし、ヌルであればロックを取得する
  3. ヌルチェックとロック取得の間に有効なポインタになる可能性があるためにロック取得後にさらにヌルチェックをし、ヌルであれば初期化をする

こういった技法は非常に難しく、潜在的な並行性バグの可能性があるためにユーザーの行動を制限していた。

そのために、言語によってstatic変数の初期化がスレッドセーフであることを保証することとなった。

参照