4

次の Singleton 実装に何か問題がありますか?

Foo& Instance() {
    if (foo) {
        return *foo;
    }
    else {
        scoped_lock lock(mutex);

        if (foo) {
            return *foo;
        }
        else {
            // Don't do foo = new Foo;
            // because that line *may* be a 2-step 
            // process comprising (not necessarily in order)
            // 1) allocating memory, and 
            // 2) actually constructing foo at that mem location.
            // If 1) happens before 2) and another thread
            // checks the foo pointer just before 2) happens, that 
            // thread will see that foo is non-null, and may assume 
            // that it is already pointing to a a valid object.
            //
            // So, to fix the above problem, what about doing the following?

            Foo* p = new Foo;
            foo = p; // Assuming no compiler optimisation, can pointer 
                     // assignment be safely assumed to be atomic? 
                     // If so, on compilers that you know of, are there ways to 
                     // suppress optimisation for this line so that the compiler
                     // doesn't optimise it back to foo = new Foo;?
        }
    }
    return *foo;
}
4

7 に答える 7

4

foo = p;いいえ、それがアトミックであると想定することさえできません。32 ビット ポインターの 16 ビットをロードし、残りをロードする前にスワップ アウトする可能性があります。

その時点で別のスレッドが忍び込んで を呼び出すと、ポインターが無効になるInstance()ため、トーストされます。foo

真のセキュリティのためには、ポインターが構築された後でもミューテックスを使用することを意味しますが、テストと設定のメカニズム全体を保護する必要があります。言い換えれば(そして、scoped_lock()ここで範囲外になったときにロックが解放されると想定しています(Boostの経験はほとんどありません))、次のようなものです:

Foo& Instance() {
    scoped_lock lock(mutex);
    if (foo != 0)
        foo = new Foo();
    return *foo;
}

ミューテックスが必要ない場合 (おそらくパフォーマンス上の理由から)、私が過去に使用したオプションは、スレッド化を開始する前にすべてのシングルトンを構築することです。

つまり、その制御権があると仮定すると (そうでない場合もあります)、main他のスレッドを開始する前に各シングルトンのインスタンスを作成するだけです。次に、ミューテックスをまったく使用しないでください。その時点でスレッドの問題は発生せず、正規の don't-care-about-threads-at-all バージョンを使用できます。

Foo& Instance() {
    if (foo != 0)
        foo = new Foo();
    return *foo;
}

そして、そうです、これはあなたの API ドキュメントをわざわざ読むことができなかった人々にとってあなたのコードをより危険なものにしますが、(IMNSHO) 彼らは得たものすべてに値します :-)

于 2010-08-17T03:05:59.963 に答える
3

シンプルにしてみませんか?

Foo& Instance()
{
    scoped_lock lock(mutex);

    static Foo instance;
    return instance;
}

編集:スレッドが言語に導入されたC++ 11では。以下はスレッドセーフです。この言語は、インスタンスが一度だけ初期化され、スレッドセーフな方法で行われることを保証します。

Foo& Instance()
{
    static Foo instance;
    return instance;
}

そのため、遅延評価されます。スレッドセーフです。とてもシンプルです。勝つ/勝つ/勝つ。

于 2010-08-17T03:41:05.557 に答える
1

これは、使用しているスレッド ライブラリによって異なります。C++0x を使用している場合は、アトミックな比較とスワップ操作を使用し、バリアを記述して、ダブルチェック ロックが機能することを保証できます。POSIX スレッドまたは Windows スレッドを使用している場合は、おそらくそれを行う方法を見つけることができます。より大きな問題は、なぜですか?通常、シングルトンは不要です。

于 2010-08-17T03:40:54.297 に答える
0

コードに問題はありません。scoped_lockの後、そのセクションにはスレッドが1つしかないため、最初に入るスレッドはfooを初期化して戻り、次に2番目のスレッド(存在する場合)が入ると、fooがnullではなくなったため、すぐに戻ります。

編集:簡略化されたコードを貼り付けました。

Foo& Instance() {
  if (!foo) {
    scoped_lock lock(mutex);
    // only one thread can enter here
    if (!foo)
        foo = new Foo;
  }
  return *foo;
}
于 2010-08-18T05:12:59.930 に答える
0

1 つのスレッドのみが作成を試行するように、実際のミューテックスを使用してみませんfoo

Foo& Instance() {
    if (!foo) {
        pthread_mutex_lock(&lock);
        if (!foo) {
            Foo *p = new Foo;
            foo = p;
        }
        pthread_mutex_unlock(&lock);
    }
    return *foo;
}

これは、無料のリーダーを使用したテストとテストと設定のロックです。非アトミック置換環境で読み取りの安全性を保証する場合は、上記をリーダー/ライター ロックに置き換えます。

edit : 本当に無料のリーダーが必要な場合は、foo最初に書き込み、次にフラグ variable を書き込みますfooCreated = 1。チェックfooCreated != 0は安全です。の場合fooCreated != 0foo初期化されます。

Foo& Instance() {
    if (!fooCreated) {
        pthread_mutex_lock(&lock);
        if (!fooCreated) {
            foo = new Foo;
            fooCreated = 1;
        }
        pthread_mutex_unlock(&lock);
    }
    return *foo;
}
于 2010-08-17T02:59:39.127 に答える
0

C++のnew演算子は常に 2 段階のプロセスを呼び出します:
1.) シンプルなプロセスと同じメモリを割り当てますmalloc
2.) 指定されたデータ型のコンストラクターを呼び出します

Foo* p = new Foo;
foo = p;

上記のコードは、シングルトンの作成を 3 つのステップにします。これは、解決しようとしている問題に対して脆弱です。

于 2010-08-17T03:07:47.930 に答える
0

ご意見ありがとうございます。Joe Duffy の優れた「Concurrent Programming on Windows」を参照した後、以下のコードを使用する必要があると考えています。一部の名前の変更と InterlockedXXX 行を除いて、ほとんどは彼の本のコードです。次の実装では以下を使用します。

  1. 一時ポインターと「実際の」ポインターの両方にvolatileキーワードを追加して、コンパイラーからの並べ替えから保護します。
  2. CPUからの並べ替えから保護するためのInterlockedCompareExchangePointer

だから、それはかなり安全なはずです(...そうですか?):

template <typename T>
class LazyInit {
public:
    typedef T* (*Factory)();
    LazyInit(Factory f = 0) 
        : factory_(f)
        , singleton_(0)
    {
        ::InitializeCriticalSection(&cs_);
    }

    T& get() {
        if (!singleton_) {
            ::EnterCriticalSection(&cs_);
            if (!singleton_) {
                T* volatile p = factory_();
                // Joe uses _WriterBarrier(); then singleton_ = p;
                // But I thought better to make singleton_ = p atomic (as I understand, 
                // on Windows, pointer assignments are atomic ONLY if they are aligned)
                // In addition, the MSDN docs say that InterlockedCompareExchangePointer
                // sets up a full memory barrier.
                ::InterlockedCompareExchangePointer((PVOID volatile*)&singleton_, p, 0);
            }
            ::LeaveCriticalSection(&cs_);
        }
        #if SUPPORT_IA64
        _ReadBarrier();
        #endif
        return *singleton_;
    }

    virtual ~LazyInit() {
        ::DeleteCriticalSection(&cs_);
    }
private:
    CRITICAL_SECTION cs_;
    Factory factory_;
    T* volatile singleton_;
};
于 2010-08-18T04:55:02.050 に答える