38

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

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

(私は自分の C++ を十分に理解していませんが、コードが実行される前に整数および定数の静的変数が初期化される場合 (つまり、静的コンストラクターが実行される前であっても - それらの値はプログラムで既に「初期化」されている可能性があります)もしそうなら - おそらくこれは、シングルトンミューテックスを実装するために悪用される可能性があります - これは、実際のシングルトンの作成を保護するために使用できます..)


すばらしい、私は今、いくつかの良い答えを持っているようです (残念ながら、2 つまたは 3 つを答えとしてマークすることはできません)。2 つの大まかな解決策があるようです。

  1. POD 静的変数の静的初期化 (動的初期化ではなく) を使用し、組み込みのアトミック命令を使用して独自のミューテックスを実装します。これは私の質問でほのめかしていた解決策のタイプであり、私はすでに知っていたと思います.
  2. pthread_onceboost::call_onceなどの他のライブラリ関数を使用します。これらは確かに知りませんでした-そして、投稿された回答に非常に感謝しています.
4

9 に答える 9

14

残念ながら、Matt の回答には、C/C++ メモリ モデルではサポートされていないダブルチェック ロックと呼ばれるものが含まれています。(これは、Java 1.5 以降 (.NET だと思います) のメモリ モデルでサポートされています。) これは、pObj == NULLチェックが行われてからロック (ミューテックス) が取得されるまでの間pObjに、別のスレッドで既に割り当てられている可能性があることを意味します。 . スレッドの切り替えは、プログラムの「行」間ではなく、OS が必要とするたびに発生します (ほとんどの言語では、コンパイル後の意味はありません)。

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

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

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 つ) を安全に作成し、エクストラを破棄できる場合にのみ機能します。OSAtomicCompareAndSwapPtrBarrierMac OS X で提供される関数 (ほとんどのオペレーティング システムは同様のプリミティブを提供します) は、 であるかどうかをチェックしpObjNULL実際に設定されtempている場合にのみ設定します。これは、ハードウェアサポートを使用して、実際には文字通り一度だけスワップを実行し、それが発生したかどうかを通知します。

OS がこれら 2 つの極端な中間にある機能を提供している場合に利用できるもう 1 つの機能は、pthread_once. これにより、基本的にすべてのロック/バリア/な​​どを実行することにより、一度だけ実行される機能を設定できます。何回呼び出されても、何スレッドで呼び出されても。

于 2008-08-09T23:09:01.400 に答える
13

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

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

C++ 標準の 2003 年版から:

静的ストレージ期間 (3.7.1) を持つオブジェクトは、他の初期化が行われる前にゼロで初期化されます (8.5)。ゼロ初期化と定数式による初期化をまとめて静的初期化と呼びます。他のすべての初期化は動的初期化です。定数式 (5.19) で初期化された静的ストレージ期間を持つ POD 型 (3.9) のオブジェクトは、動的初期化が行われる前に初期化されなければなりません。同じ翻訳単位の名前空間スコープで定義され、動的に初期化される静的保存期間を持つオブジェクトは、それらの定義が翻訳単位に現れる順序で初期化されます。

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

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

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

于 2008-08-09T23:52:40.253 に答える
12

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

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も参照してください。

于 2010-05-19T16:20:12.490 に答える
8

静的変数なしでそれを行うことはできませんが、静的変数を許容する場合は、この目的でBoost.Threadを使用できます。詳細については、「1回限りの初期化」セクションをお読みください。

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

于 2008-08-10T04:13:08.543 に答える
6

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

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

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

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

また、余談かもしれませんが、静的な非 POD 型は常に避けるのが最善です。(PODへのポインタはOKです。)これには多くの理由があります。あなたが言及したように、初期化順序は定義されていません-デストラクタが呼び出される順序も定義されていません。このため、プログラムは終了しようとするとクラッシュしてしまいます。多くの場合、大したことではありませんが、使用しようとしているプロファイラーがクリーンな終了を必要とする場合、ショーストッパーになることがあります。

于 2008-09-16T10:21:51.773 に答える
1
  1. 弱いメモリモデルで読み取ります。ダブルチェックされたロックとスピンロックを壊す可能性があります。Intelは(まだ)強力なメモリモデルであるため、Intelではより簡単です

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

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

  4. そのようなオブジェクトは適切に破壊するのが難しい

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

于 2009-11-09T17:32:29.957 に答える
1

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

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

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

編集:うんデレク、その通りです。私の悪い。:)

于 2008-08-10T04:34:59.793 に答える
1

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

OJ、それはうまくいきません。クリスが指摘したように、これはダブルチェックロックであり、現在のC++標準での動作が保証されていません。参照:C++とダブルチェックロックの危険性

編集:問題ありません、OJ。それが機能する言語では本当に素晴らしいです。とても便利なイディオムなので、C ++ 0xで動作することを期待しています(確かではありませんが)。

于 2008-08-10T05:11:03.247 に答える
0

main()安全ではなく、このようなものを初期化するだけではそれほど人気が​​ないよりも頻繁に壊れてしまうため、これを行わないでくださいと言っていると思います。

(そうです、それを提案するということは、グローバルオブジェクトのコンストラクターで面白いことをしようとしてはいけないということを私は知っています。それがポイントです。)

于 2008-08-20T16:13:40.890 に答える