質問

C++ でシングルトン オブジェクトを実装する方法はありますか。

  1. スレッドセーフな方法で遅延構築されます (2 つのスレッドが同時にシングルトンの最初のユーザーになる可能性がありますが、それでも構築されるのは 1 回のみです)。
  2. 事前に構築される静的変数には依存しません (そのため、静的変数の構築中にシングルトン オブジェクト自体を安全に使用できます)。

(C++ のことはよくわかりませんが、整数静的変数と定数静的変数は、コードが実行される前に (つまり、静的コンストラクターが実行される前でも) 初期化されるのでしょうか。それらの値はプログラム内ですでに「初期化」されている可能性があります。画像)?そうであれば、おそらくこれを利用してシングルトン ミューテックスを実装できる可能性があり、これを実際のシングルトンの作成を保護するために使用できます。)


素晴らしいですね。今では良い答えがいくつかあるようです (2 つまたは 3 つをマークできないのが残念です) 答え)。大きく分けて 2 つの解決策があるようです。

  1. POD 静的変数の静的初期化 (動的初期化ではなく) を使用し、組み込みのアトミック命令を使用してそれを使用して独自のミューテックスを実装します。これは私が質問でほのめかした解決策の一種であり、すでに知っていたと思います。
  2. 次のような他のライブラリ関数を使用してください pthread_once または boost::call_once. 。これらについては私はまったく知りませんでした。投稿された回答にとても感謝しています。
役に立ちましたか?

解決

基本的に、同期 (以前に構築された変数) を使用せずに、シングルトンの同期作成を要求しています。一般に、いいえ、これは不可能です。同期に利用できるものが必要です。

他の質問に関しては、はい、静的に初期化できる静的変数です(つまり、ランタイム コードは必要ありません) は、他のコードが実行される前に初期化されることが保証されます。これにより、静的に初期化されたミューテックスを使用してシングルトンの作成を同期できるようになります。

C++ 標準の 2003 年リビジョンより:

静的ストレージ期間 (3.7.1) を持つオブジェクトは、他の初期化が行われる前にゼロ初期化 (8.5) されます。ゼロ初期化と定数式による初期化を総称して静的初期化と呼びます。他のすべての初期化は動的初期化です。定数式 (5.19) で初期化された静的ストレージ期間を持つ POD タイプ (3.9) のオブジェクトは、動的初期化が行われる前に初期化されます。同じ翻訳単位内の名前空間スコープで定義され、動的に初期化される静的記憶期間を持つオブジェクトは、その定義が翻訳単位に現れる順序で初期化されます。

もし、あんたが 知る 他の静的オブジェクトの初期化中にこのシングルトンを使用することになるので、同期が問題にならないことがわかると思います。私の知る限り、すべての主要なコンパイラーは単一スレッドで静的オブジェクトを初期化するため、静的初期化中のスレッドセーフが保たれます。シングルトン ポインターを NULL として宣言し、使用する前に初期化されているかどうかを確認できます。

ただし、これは次のことを前提としています。 知る このシングルトンは静的初期化中に使用することになります。これも標準で保証されていないため、完全に安全を確保したい場合は、静的に初期化されたミューテックスを使用してください。

編集:アトミック コンペア アンド スワップを使用するという Chris の提案は、確かに機能するでしょう。移植性が問題でない場合 (および追加の一時シングルトンの作成が問題でない場合)、これはオーバーヘッドがわずかに低いソリューションです。

他のヒント

残念ながら、マットの答えには、いわゆる 二重チェックロック これは C/C++ メモリ モデルではサポートされていません。(これは Java 1.5 以降 (そして私は .NET だと思います) のメモリ モデルでサポートされています。) これは、 pObj == NULL チェックが行われ、ロック (ミューテックス) が取得されると、 pObj すでに別のスレッドに割り当てられている可能性があります。スレッドの切り替えは、プログラムの「行」間ではなく (ほとんどの言語ではコンパイル後に意味を持ちません)、OS が望むときにいつでも行われます。

さらに、マットも認めているように、彼は int OS プリミティブではなくロックとして。そんなことはしないでください。適切なロックには、メモリ バリア命令、場合によってはキャッシュライン フラッシュなどの使用が必要です。ロックにはオペレーティング システムのプリミティブを使用します。使用されるプリミティブは、オペレーティング システムが実行される個々の CPU ライン間で変わる可能性があるため、これは特に重要です。CPU Foo で動作するものは、CPU Foo2 では動作しない可能性があります。ほとんどのオペレーティング システムは、POSIX スレッド (pthread) をネイティブにサポートするか、OS スレッド パッケージのラッパーとして提供するため、多くの場合、POSIX スレッド (pthread) を使用する例を示すことが最善です。

オペレーティング システムが適切なプリミティブを提供しており、パフォーマンスのためにそれが絶対に必要な場合は、このタイプのロック/初期化を行う代わりに、 アトミックな比較と交換 共有グローバル変数を初期化する操作。基本的に、書く内容は次のようになります。

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

これは、シングルトンの複数のインスタンス (GetSingleton() を同時に呼び出すスレッドごとに 1 つ) を作成し、余分なものを破棄しても安全な場合にのみ機能します。の OSAtomicCompareAndSwapPtrBarrier Mac OS X で提供される関数 — ほとんどのオペレーティング システムが同様のプリミティブを提供しています — pObjNULL そして実際にそれを設定するだけです temp そうであればそれに。これは、文字通りスワップのみを実行するためにハードウェア サポートを使用します。 一度 そしてそれが起こったかどうかを伝えてください。

OS がこれら 2 つの極端な中間にある機能を提供している場合に活用できるもう 1 つの機能は、次のとおりです。 pthread_once. 。これにより、基本的にすべてのロック/バリアなどを実行することで、一度だけ実行される関数をセットアップできます。何回呼び出されたか、または呼び出されたスレッドの数に関係なく、あなたにとっては策略です。

以下は、非常に単純な遅延構築されたシングルトン ゲッターです。

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

これは遅延であり、次の C++ 標準 (C++0x) ではスレッド セーフであることが要求されます。実際、少なくとも g++ はこれをスレッドセーフな方法で実装すると信じています。それがあなたのターゲットコンパイラである場合 または これをスレッドセーフな方法で実装するコンパイラを使用している場合 (新しい Visual Studio コンパイラでも実装されているでしょうか?)わかりません)、必要なのはこれだけかもしれません。

こちらもご覧ください http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html このトピックにおいて。

静的変数なしではこれを行うことはできませんが、静的変数を許容できる場合は、次のように使用できます。 ブーストスレッド この目的のために。詳細については、「ワンタイム初期化」セクションを参照してください。

次に、シングルトン アクセサー関数で、次を使用します。 boost::call_once オブジェクトを構築して返します。

gcc の場合、これはかなり簡単です。

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

GCC は、初期化がアトミックであることを確認します。 VC++ の場合、これは当てはまりません. :-(

このメカニズムの大きな問題の 1 つは、テスト容易性の欠如です。テスト間で LazyType を新しいものにリセットする必要がある場合、または LazyType* を MockLazyType* に変更したい場合は、それはできません。このことを考慮すると、通常は静的ミューテックス + 静的ポインターを使用するのが最善です。

また、おそらく余談ですが、静的な非 POD タイプは常に避けることが最善です。(POD へのポインタは問題ありません。)これには多くの理由があります。おっしゃるとおり、初期化順序は定義されていません。ただし、デストラクターが呼び出される順序も定義されていません。このため、プログラムは終了しようとするとクラッシュしてしまいます。多くの場合、大した問題ではありませんが、使用しようとしているプロファイラーを完全に終了する必要がある場合には、場合によっては重大な問題が発生することがあります。

この質問にはすでに答えられていますが、他にも言及すべき点がいくつかあると思います。

  • 動的に割り当てられたインスタンスへのポインターを使用しながら、シングルトンの遅延インスタンス化が必要な場合は、適切な時点でそれをクリーンアップするようにする必要があります。
  • Matt のソリューションを使用することもできますが、ロックには適切なミューテックス/クリティカル セクションを使用し、ロックの前後で「pObj == NULL」をチェックする必要があります。もちろん、 pObj もあるはずだ 静的 ;)。この場合、ミューテックスは不必要に重くなるため、クリティカル セクションを使用する方がよいでしょう。

ただし、すでに述べたように、少なくとも 1 つの同期プリミティブを使用しない限り、スレッドセーフな遅延初期化を保証することはできません。

編集:そう、デレク、あなたは正しい。悪いです。:)

Matt のソリューションを使用することもできますが、ロックには適切なミューテックス/クリティカル セクションを使用し、ロックの前後で「pObj == NULL」をチェックする必要があります。もちろん、pObj も静的である必要があります ;) 。この場合、ミューテックスは不必要に重くなるため、クリティカル セクションを使用する方がよいでしょう。

OJ、それはうまくいきません。Chris が指摘したように、これは二重チェック ロックであり、現在の C++ 標準で動作することが保証されていません。見る: C++ と二重チェックされたロックの危険性

編集:問題ありません、OJ。実際に機能する言語では本当に便利です。これは非常に便利なイディオムなので、C++0x でも動作すると思います (確信はありませんが)。

  1. 弱いメモリモデルで読み取ります。ダブルチェックされたロックやスピンロックを破壊する可能性があります。Intel は (まだ) 強力なメモリ モデルなので、Intel では簡単です

  2. 「volatile」を慎重に使用して、オブジェクトの一部をレジスタにキャッシュしないようにしてください。そうしないと、オブジェクト ポインタは初期化されてしまいますが、オブジェクト自体は初期化されず、他のスレッドがクラッシュします。

  3. 静的変数の初期化と共有コードの読み込みの順序は、簡単ではない場合があります。オブジェクトを破棄するコードがすでにアンロードされているため、終了時にプログラムがクラッシュするケースを見たことがあります。

  4. そのようなオブジェクトを適切に破壊するのは困難です

一般に、シングルトンは適切に実行するのが難しく、デバッグも困難です。それらは完全に避けた方が良いでしょう。

これは安全ではなく、単に初期化するよりも頻繁に壊れる可能性があるため、これを行わないでください。 main() そんなに人気はないだろう。

(そして、はい、それを示唆するということは、グローバル オブジェクトのコンストラクターで興味深いことを試みるべきではないことを意味していることはわかっています。それがポイントです。)

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top