C++ でスレッドセーフなシングルトン パターンを作成するにはどうすればよいですか?

スレッドセーフなシングルトン パターンを作成するにはどうすればよいですか?

シングルトンパターンの簡単な実装

シングルトン パターンは、おそらく最も広く流通しているデザイン パターンの 1 つです。単純な実装コードは次のようになります。

class singleton
{
public:
	static singleton* instance()
	{
		if (inst_ != nullptr) { 
			inst_ = new singleton();
		}
		return inst_;
	}
private:
	singleton(){}
	static singleton* inst_;
};

singleton* singleton::inst_ = nullptr;

このコードはシングルスレッド環境ではまったく問題ありませんが、マルチスレッド環境では状況が少し異なります。次の実行順序を考慮してください。

  1. (inst_ != nullptr) の場合、スレッド 1 が実行された後、ハングします。
  2. スレッド 2 はインスタンス関数を実行します。inst_ が割り当てられていないため、プログラムは inst_ = new singleton () ステートメントを実行します。
  3. スレッド 1 が再開され、inst_ = new singleton() ステートメントが再度実行され、シングルトン ハンドルが複数回作成されます。

したがって、そのような実装はスレッドセーフではありません。

問題のある二重チェックされたロック

マルチスレッドの問題を解決するための最も一般的な方法は、ロックを追加することです。次に、次の実装バージョンを簡単に取得できます。

class singleton
{
public:
	static singleton* instance()
	{
		guard<mutex> lock{ mut_ };
		if (inst_ != nullptr) {
			inst_ = new singleton();
		}
		return inst_;
	}
private:
	singleton(){}
	static singleton* inst_;
	static mutex mut_;
};

singleton* singleton::inst_ = nullptr;
mutex singleton::mut_;

この問題は解決されましたが、パフォーマンスはそれほど満足できるものではなく、結局、インスタンスを使用するたびにロックとロック解除の追加コストが発生します。さらに重要なのは、このロックは毎回必要ないということです。実際、ロックする必要があるのはシングルトン インスタンスを作成するときだけであり、後で使用するときにロックする必要はまったくありません。したがって、誰かが二重検出ロックを記述する方法を提案しました。

...
	static singleton* instance()
	{
		if (inst_ != nullptr) {
			guard<mutex> lock{ mut_ };
			if (inst_ != nullptr) {
				inst_ = new singleton();
			}
		}
		return inst_;
	}
...

まず inst_ が初期化されているかどうかを確認し、初期化されていない場合はロックの初期化プロセスに進みます。このように、コードは少し奇妙に見えますが、シングルトンの作成時にのみロック オーバーヘッドを導入するという目的は達成されているようです。残念ながら、このアプローチには問題があります。Scott Meyers と Andrei Alexandrescu は、「C++ と二重チェック ロックの危険性 」という 記事でこの問題について詳しく説明しています。ここでは簡単な説明のみに留めます。問題は次の点にあります。

	inst_ = new singleton();

この行。このコードはアトミックではなく、通常は次の 3 つのステップに分かれています。

  1. 演算子 new を呼び出して、シングルトン オブジェクトにメモリ領域を割り当てます。
  2. 割り当てられたメモリ空間でシングルトンのコンストラクターを呼び出します。
  3. 割り当てられたメモリ空間のアドレスを inst_ に割り当てます。

プログラムが厳密に1→2→3の手順に従ってコードを実行できるのであれば、上記の方法でも問題ありませんが、実際はそうではありません。コンパイラによる最適化された命令の並べ替えと、CPU命令のアウト・オブ・オーダー実行(具体例については「【マルチスレッドのこと】マルチスレッドの実行順序は期待どおりですか?」を参照してください)です。ステップ 3 をステップ 3 よりも早く実行することが可能 2. 次の実行順序を考慮してください。

  1. スレッド 1 は、ステップ 1 -> 3 -> 2 の順序で実行され、ステップ 1 と 3 の実行後に一時停止されます。
  2. スレッド 2 はインスタンス関数を実行して、以降の操作のためのシングルトン ハンドルを取得します。

inst_ はスレッド 1 で割り当てられているため、空ではない inst_ インスタンスをスレッド 2 で取得でき、操作は続行されます。しかし実際には、シングルトン オブジェクトの作成は完了しておらず、この時点での操作は未定義です。

最新の C++ での回避策

最新の C++ では、次のメソッドを通じてスレッドセーフで効率的なシングルトン モードを実装できます。

最新の C++ でのメモリ順序制約の使用

最新の C++ では、6 つのメモリ実行シーケンスが指定されています。メモリ順序制限を合理的に使用すると、コード命令の再配置を回避できます。考えられる実装は次のとおりです。

class singleton {
public:
	static singleton* instance()
	{
		singleton* ptr = inst_.load(memory_order_acquire);
		if (ptr == nullptr) {
			lock_guard<mutex> lock{ mut_ };
			ptr = inst_.load(memory_order_relaxed);
			if (ptr == nullptr) {
				ptr = new singleton();
				inst_.store(ptr, memory_order_release);
			}
		}
	
		return inst_;
	}
private:
	singleton(){};
	static mutex mut_;
	static atomic<singleton*> inst_;
};

mutex singleton::mut_;
atomic<singleton*> singleton::inst_;

アセンブリ コードを見てください。

ご覧のとおり、コンパイラは命令の実行順序を保証するために必要なステートメントを挿入しました。

最新の C++ での call_once メソッドの使用

call_once も最新の C++ に導入された新機能で、関数が 1 回だけ実行されるようにすることができます。call_once を使用したコード実装は次のとおりです。

class singleton
{
public:
	static singleton* instance()
	{
		if (inst_ != nullptr) {
			call_once(flag_, create_instance);
		}
		return inst_;
	}
private:
	singleton(){}
	static void create_instance()
	{
		inst_ = new singleton();
	}
	static singleton* inst_;
	static once_flag flag_;
};

singleton* singleton::inst_ = nullptr;
once_flag singleton::flag_;

アセンブリ コードを見てください。

プログラムが最後に __gthrw_pthread_once を呼び出して、関数が 1 回だけ実行されるようにしていることがわかります。

静的ローカル変数を使用する

現在、C++ には変数の初期化順序に関して次の規則があります。

変数の初期化中に制御が同時に宣言に入った場合、同時実行は初期化の完了を待機します。

したがって、静的ローカル変数を使用するだけで、スレッドセーフなシングルトン パターンを実装できます。

class singleton
{
public:
	static singleton* instance()
	{
		static singleton inst_;
		return &inst_;
	}
private:
	singleton(){}
};

アセンブリ コードを見てください。

静的ローカル変数初期化のマルチスレッド安全性を確保するために、コンパイラーが関連コードを自動的に挿入していることがわかります。

全文は以上です。

おすすめ

転載: blog.csdn.net/weixin_47367099/article/details/127458787