14

私は次の記事を読んでいまし

「マルチスレッド コードで .NET Int32 を読み取るときに、.NET Int32 をロックする必要がありますか?」

記事で説明されているように、それが 32 ビット SO の Int64 である場合、破損する可能性があることを理解しています。しかし、Int32 の場合、次の状況を想像しました。

class Test
{
  private int example = 0;
  private Object thisLock = new Object();

  public void Add(int another)
  {
    lock(thisLock)
    {
      example += another;
    }
  }

  public int Read()
  {
     return example;
  }
}

Read メソッドにロックを含める理由がわかりません。あなたは?

更新(Jon Skeet および ctacke による) 回答に基づいて、上記のコードがマルチプロセッサ キャッシュに対して依然として脆弱であることを理解しています (各プロセッサには独自のキャッシュがあり、他のプロセッサとは同期されていません)。以下の 3 つの変更はすべて、問題を修正します。

  1. 「int example」に「volatile」プロパティを追加する
  2. Thread.MemoryBarrier(); の挿入 「int example」を実際に読む前に
  3. 「lock(thisLock)」内の「int example」を読む

また、「揮発性」が最もエレガントなソリューションだと思います。

4

6 に答える 6

25

ロックにより、次の 2 つのことが達成されます。

  • これはミューテックスとして機能するため、一度に 1 つのスレッドのみが一連の値を変更するようにすることができます。
  • あるスレッドによって行われたメモリ書き込みが別のスレッドで確実に見えるようにするメモリ バリア (取得/解放セマンティクス) を提供します。

ほとんどの人は、最初の点は理解していますが、2 番目の点は理解していません。質問のコードを 2 つの異なるスレッドから使​​用したとします。一方のスレッドはAdd繰り返し呼び出し、もう一方のスレッドは を呼び出しReadます。アトミシティ自体は、最終的に 8 の倍数しか読み取らAddないことを保証します。また、ロックを呼び出すスレッドが 2 つある場合は、追加を「失う」ことがないようにします。ただし、スレッドが数回呼び出されReadた後でも、スレッドが 0 しか読み取らない可能性は十分にあります。Addメモリ バリアがなければ、JIT は値をレジスタにキャッシュするだけで、読み取り間で値が変更されていないと想定できます。メモリ バリアのポイントは、何かが実際にメイン メモリに書き込まれるか、または実際にメイン メモリから読み取られるかを確認することです。

メモリ モデルはかなり複雑になる可能性がありますが、共有データに (読み取りまたは書き込みのために)アクセスするたびにロックを解除するという単純な規則に従えば、問題はありません。詳細については、私のスレッド化チュートリアルの揮発性/原子性の部分を参照してください。

于 2008-12-27T19:15:00.670 に答える
7

それはすべて文脈に依存します。整数型または参照を扱う場合、System.Threading.Interlockedクラスのメンバーを使用したい場合があります。

次のような典型的な使用法:

if( x == null )
  x = new X();

Interlocked.CompareExchange()への呼び出しに置き換えることができます:

Interlocked.CompareExchange( ref x, new X(), null);

Interlocked.CompareExchange() は、比較と交換がアトミック操作として行われることを保証します。

Add()Decrement()Exchange()Increment()Read()などの Interlocked クラスの他のメンバーはすべて、それぞれの操作をアトミックに実行します。MSDNのドキュメントを参照してください。

于 2008-12-27T19:28:22.297 に答える
4

32ビットの数値をどのように使用するかによって異なります。

次のような操作を実行したい場合:

i++;

それは暗黙的に次のように分解されます

  1. の値を読むi
  2. 1つ追加
  3. 収納i

別のスレッドが i を 1 の後、3 の前に変更した場合、i が 7 だったところに問題が発生し、それに 1 を追加すると、現在は 492 になります。

しかし、単に i を読んでいる場合、または次のような単一の操作を実行している場合:

i = 8;

その後、iをロックする必要はありません。

さて、あなたの質問は「...それを読むときに.NET Int32をロックする必要がある...」と言いますが、あなたの例には、Int32への読み取りと書き込みが含まれます。

だから、それはあなたが何をしているかに依存します。

于 2008-12-27T18:40:50.303 に答える
2

アトミックにする必要がある場合は、ロックが必要です。読み取りと書き込み (i++ を実行する場合などの対になった操作として) は、キャッシングのために 32 ビットの数値がアトミックであることが保証されません。さらに、個々の読み取りまたは書き込みは、必ずしもレジスターに直接送信されるとは限りません (ボラティリティ)。揮発性にしても、整数を変更したい場合 (読み取り、インクリメント、書き込み操作など) には原子性が保証されません。整数の場合、ミューテックスまたはモニターが重すぎる場合があり (ユースケースによって異なります)、それがInterlocked クラスの目的です。これらのタイプの操作の原子性が保証されます。

于 2008-12-27T19:29:17.117 に答える
2

スレッドロックが 1 つしかない場合、何も達成されません。ロックの目的は他のスレッドをブロックすることですが、誰もロックをチェックしなければ機能しません!

書き込みはアトミックであるため、32 ビットの int でメモリ破損を心配する必要はありませんが、必ずしもロックフリーになるとは限りません。

あなたの例では、疑わしいセマンティクスを取得することが可能です:

example = 10

Thread A:
   Add(10)
      read example (10)

Thread B:
   Read()
      read example (10)

Thread A:
      write example (10 + 10)

これは、スレッド A が更新を開始した後にスレッド B がexample の値の読み取りを開始したことを意味しますが、更新前の値を読み取ります。それが問題になるかどうかは、このコードが何をすべきかによると思います。

これはサンプル コードであるため、問題がわかりにくい場合があります。しかし、正規のカウンター関数を想像してみてください:

 class Counter {
    static int nextValue = 0;

    static IEnumerable<int> GetValues(int count) {
       var r = Enumerable.Range(nextValue, count);
       nextValue += count;
       return r;
    }
 }

次に、次のシナリオ。

 nextValue = 9;

 Thread A:
     GetValues(10)
     r = Enumerable.Range(9, 10)

 Thread B:
     GetValues(5)
     r = Enumerable.Range(9, 5)
     nextValue += 5 (now equals 14)

 Thread A:
     nextValue += 10 (now equals 24)

nextValue は適切にインクリメントされますが、返される範囲は重複します。19 から 24 の値は返されませんでした。これを修正するには、var r と nextValue の割り当てをロックして、他のスレッドが同時に実行されないようにします。

于 2008-12-27T18:43:25.243 に答える
0

一般に、ロックは値が変更される場合にのみ必要です

編集: Mark Brackettの優れた要約はより適切です:

「そうでなければ非アトミック操作をアトミックにしたい場合は、ロックが必要です」

この場合、32 ビット マシンで 32 ビット整数を読み取ることは、おそらくすでにアトミック操作です... しかし、そうではないかもしれません! おそらくvolatileキーワードが必要かもしれません。

于 2008-12-27T18:16:58.713 に答える