10

スレッドセーフに取り組んでいると、ロックブロックでコードを実行する前に常に「二重チェック」を行っていることに気づき、自分が正しいことをしているかどうか疑問に思いました。同じことを行う次の 3 つの方法を検討してください。

例 1:

private static SomeCollection MyCollection;
private static Object locker;
private void DoSomething(string key)
{
    if(MyCollection[key] == null)
    {
         lock(locker)
         {
              MyCollection[key] = DoSomethingExpensive(); 
         }
    }
    DoSomethingWithResult(MyCollection[key]);
}

例 2:

private static SomeCollection MyCollection;
private static Object locker;
private void DoSomething(string key)
{
    lock(locker)
    {
         if(MyCollection[key] == null)
         {
              MyCollection[key] = DoSomethingExpensive(); 
         }
    }
    DoSomethingWithResult(MyCollection[key]);
}

例 3:

private static SomeCollection MyCollection;
private static Object locker;
private void DoSomething(string key)
{
    if(MyCollection[key] == null)
    {
        lock(locker)
        {
             if(MyCollection[key] == null)
             {
                  MyCollection[key] = DoSomethingExpensive(); 
             }
        }
    }
    DoSomethingWithResult(MyCollection[key]);
}

私は常に例 3 に傾倒しています。これが、私が正しいことをしていると思う理由です。

  • スレッド 1 が入りますDoSomething(string)
  • MyCollection[key] == nullそのため、スレッド 2 が入るのと同じように、スレッド 1 がロックを取得します。
  • MyCollection[key] == nullは依然として true であるため、スレッド 2 はロックの取得を待機します
  • スレッド 1 は値を計算MyCollection[key]し、コレクションに追加します
  • スレッド 1 はロックを解除して呼び出しますDoSomethingWithResult(MyCollection[key]);
  • スレッド 2 がロックを取得します。MyCollection[key] != null
  • スレッド 2 は何もせず、ロックを解放し、引き続き楽しく作業を続けます

例 1 は機能しますが、スレッド 2 が を冗長に計算する大きなリスクがありますMyCollection[key]

例 2 は機能しますが、必要がなくてもすべてのスレッドがロックを取得します。これは (確かに非常に小さい) ボトルネックになる可能性があります。必要がないのに、なぜスレッドを保留にするのですか?

私はこれを考えすぎていますか?もしそうなら、これらの状況を処理するための好ましい方法は何ですか?

4

4 に答える 4

7

最初の方法は使用しないでください。お気づきのとおり、リークするため、複数のスレッドが高価なメソッドを実行することになります。そのメソッドに時間がかかるほど、別のスレッドもそれを実行するリスクが大きくなります。ほとんどの場合、これは単なるパフォーマンス上の問題ですが、場合によっては、結果のデータが後で新しいデータ セットに置き換えられるという問題もある可能性があります。

2 番目の方法が最も一般的な方法です。3 番目の方法は、データが頻繁にアクセスされてロックがパフォーマンスの問題になる場合に使用されます。

于 2012-11-07T00:17:46.590 に答える
4

問題は些細なことではないので、ある種の不確実性を紹介します。基本的に私はGuffaに同意し、2番目の例を選択します。これは、1つ目が壊れているのに対し、3つ目は最適化されているように見えますが、注意が必要なためです。そのため、ここでは3番目のものに焦点を当てます。

if (item == null)
{
    lock (_locker)
    {
        if (item == null)
            item = new Something();
    }
}

一見すると、常にロックせずにパフォーマンスが向上するように見えるかもしれませんが、メモリモデル(読み取りは書き込みの前に来るように並べ替えられる場合があります)、または積極的なコンパイラの最適化(参照)のために問題もあります。

  1. スレッドAは、値が初期化されていないことに気付いたitemため、ロックを取得して値の初期化を開始します。
  2. メモリモデル、コンパイラの最適化などにより、コンパイラによって生成されたコードは、 Aが初期化の実行を完了する前に、部分的に構築されたオブジェクトを指すように共有変数を更新できます。
  3. スレッドBは、共有変数が初期化されている(または表示されている)ことに気づき、その値を返します。スレッドBは、値がすでに初期化されていると信じているため、ロックを取得しません。Aが初期化を完了する前に変数を使用すると、プログラムがクラッシュする可能性があります。

その問題の解決策があります:

  1. 揮発性変数として定義できitemます。これにより、読み取り変数が常に最新の状態になります。Volatileは、変数の読み取りと書き込みの間にメモリバリアを作成するために使用されます。

    (.NETでのダブルチェックロックおよびC#でのシングルトンパターンの実装での揮発性修飾子の必要性を参照しください

  2. 次を使用できますMemoryBarrieritem不揮発性):

    if (item == null)
    {
        lock (_locker)
        {
            if (item == null)
            {
                var temp = new Something();
                // Insure all writes used to construct new value have been flushed.
                System.Threading.Thread.MemoryBarrier();                     
                item = temp;
            }
        }
    }
    

    MemoryBarrier現在のスレッドを実行しているプロセッサは、への呼び出しに続くメモリアクセスの後に実行するために、呼び出しの前にメモリがアクセスするような方法で命令を並べ替えることはできませんMemoryBarrier

    Thread.MemoryBarrierメソッドとこのトピックを参照してください)

更新:ダブルチェックロックは、正しく実装されている場合、C#で正常に機能しているようです。詳細については、 MSDNMSDNマガジンこの回答などの追加のリファレンスを確認してください。

于 2012-11-07T01:35:29.087 に答える
3

この問題をプロに任せて、ConcurrentDictionaryを使用することをお勧めします(私はそうすることを知っています)。それはあなたが望むことを正確に実行し、正しく動作することが保証されているGetOrAddメソッドを持っています。

于 2012-11-07T00:33:18.947 に答える
1

遅延オブジェクトの作成に使用できるさまざまなパターンがあります。これは、コード例が焦点を当てているようです。コレクションが配列のようなものである場合、またはConcurrentDictionaryコードが値が既に設定されているかどうかをアトミックにチェックし、設定されていない場合にのみ書き込むことができる場合に役立つ場合がある別のバリ​​エーションは、次のようになります。

Thing theThing = myArray[index];
if (theThing == null) // Doesn't look like it's created yet
{
  Thing tempThing = new DummyThing(); // Cheap
  lock(tempThing) // Note that the lock surrounds the CompareExchange *and* initialization
  {
    theThing = System.Threading.Interlocked.CompareExchange
       (ref myArray[index], tempThing, null);
    if (theThing == null)
    {
      theThing = new RealThing(); // Expensive
      // Place an empty lock or memory barrier here if loose memory semantics require it
      myArray[index] = theThing ;
    }
  }
}
if (theThing is DummyThing)
{
  lock(theThing) { } // Wait for thread that created DummyThing to release lock
  theThing = myArray[index];
  if (theThing is DummyThing)
      throw something; // Code that tried to initialize object failed to do so
  }
}

このコードは、 から派生した型のダミー インスタンスを安価に構築できることを前提としていますThing。新しいオブジェクトは、シングルトンにすることも、再利用することもできません。すべてのスロットはmyArray2 回書き込まれます。最初は事前にロックされたダミー オブジェクトで、次に実際のオブジェクトで書き込まれます。1 つのスレッドだけがダミー オブジェクトを書き込むことができ、ダミー オブジェクトを正常に書き込んだスレッドだけが実際のオブジェクトを書き込むことができます。他のスレッドは、実際のオブジェクト (この場合、オブジェクトは完全に初期化されます) を参照するか、実際のオブジェクトへの参照で配列が更新されるまでロックされるダミー オブジェクトを参照します。

上記の他のアプローチとは異なり、このアプローチでは、配列内のさまざまなアイテムを同時に初期化できます。ブロックされるのは、初期化が進行中のオブジェクトにアクセスしようとした場合のみです。

于 2013-08-22T20:12:52.957 に答える