2013-04-12 19 views
5

GetNextNumber()が2つのスレッドによって同時に呼び出された場合、両方のスレッドに同じ番号を返す可能性がありますか?静的変数を使用する同時実行

class Counter 
{ 
private static int s_Number = 0; 
public static int GetNextNumber() 
{ 
    s_Number++; 
    return s_Number; 
} 
} 

理由を説明できますか?

EDIT:コードが両方のスレッドに同じ番号を返すことが可能な場合は、次の点が正しいですか? s_Numberが2のときに2つのスレッドがGetNextNumber()を呼び出したとしましょう。同じ値が返された場合、その値は4だけです.3にすることはできません。

+8

はい、それはそれはそうだ –

+0

可能ですが、それは可能ですので、あなたが状況を処理する必要があります。私はlockキーワードを使用します。 – DGibbs

+5

を保証していないがそれは、可能です –

答えて

10

Whこのような単純なカウンタを扱うエン、それが使用することをお勧めしますInterlocked.Increment:これは(限り、番号が溢れないよう)一意の値を返します各スレッドを確保する、と何の増加が失われないことを

private static int s_Number = 0; 

public static int GetNextNumber() 
{ 
    return Interlocked.Increment(ref s_Number); 
} 

。読むs_Numbers_Number

  • にそれ
  • Storeに新しい値を1を追加しますs_Number
  • の既存の値を読む

    1. :元のコードは、次の手順に分けることができますので

    2. 読み取り値を返す

    起こり得るシナリオは次のとおり

    1. 両方のスレッドは、両方のスレッドが同じ値で終わる、同じ既存の値が、1だけインクリメントを読み取ることを意味しており、残りの前に、ステップ1を実行します。 増分を失います
    2. スレッドはステップ1〜3を矛盾なく実行できますが、両方のスレッドが変数を更新して同じ値を取得した後にステップ4を実行します。

      private readonly object _SomeLock = new object(); 
      
      ... 
      
      lock (_SomeLock) 
      { 
          // only 1 thread allowed in here at any one time 
          // manipulate the data structures here 
      } 
      

      しかし、このような単純な部分のため:は、より多くのデータをlock文は、通常、より良い方法であり、アトミックアクセスする必要があるコード、の大きな片について数

    をスキップInterlocked.Incrementは、素早くフィールドをインクリメントして新しい値を取得するだけです。

    クラスには他のメソッドもあります。これらのメソッドは、扱うシナリオではとても便利です。

    の詳細な説明は、インクリメントが失われたです。

    の2つのスレッドの前に0でs_Number開始を実行することを想定してみましょう:

    Thread 1       Thread 2 
    Read s_Number = 0 
                Read s_Number = 0 
    Add 1 to s_Number, getting 1 
                Add 1 to s_Number, getting 1 (same as thread 1) 
    Store into s_Number (now 1) 
                Store into s_Number (now 1) 
    Read s_Number = 1 
                Read s_Number = 1 
    Return read value (1) 
                Return read value (1) 
    

    あなたが上見ることができるように、s_Numberの最終値が2となってきたはずであり、スレッドのいずれかが返されてきたはずです1、もう一方は2です。代わりに、最終値は1で、両方のスレッドが1を返しました。ここでインクリメントが失われました。数

    Thread 1       Thread 2 
    Read s_Number = 0 
    Add 1 to s_Number, getting 1 
    Store into s_Number (now 1) 
                Read s_Number = 1 
                Add 1 to s_Number, getting 2 
                Store into s_Number (now 2) 
    Read s_Number = 2 
                Read s_Number = 2 
    Return read value (2) 
                Return read value (2) 
    

    をスキップする

    詳細な説明はここs_Numberの最終結果が正しい2、が、スレッドのいずれかが1に戻ってきたはずであろう代わりに、それらの両方が2

    を返さ

    元のコードがILレベルでどのように見えるかを見てみましょう。私は、あなたがプレイしたい場合、私は、(右下隅にある小さな/ O +)の最適化を可能にする、上記のILコードを取得するためにLINQPadを使用し、コメント

    // public static int GetNumber() 
    // { 
    GetNumber: 
    //  s_Number++; 
    IL_0000: ldsfld  UserQuery.s_Number // step 1: Read s_Number 
    IL_0005: ldc.i4.1       // step 2: Add 1 to it 
    IL_0006: add        //   (part of step 2) 
    IL_0007: stsfld  UserQuery.s_Number // step 3: Store into s_Number 
    // return s_Number; 
    IL_000C: ldsfld  UserQuery.s_Number // step 4: Read s_Number 
    IL_0011: ret        // step 5: Return the read value 
    // } 
    

    注意してIL命令に元のコードを追加しますそれはILに変換する方法を参照するには、コードで、LINQPadをダウンロードし、それをこのプログラムを養う:

    void Main() { } // Necessary for LINQPad/Compiler to be happy 
    
    private static int s_Number = 0; 
    public static int GetNumber() 
    { 
        s_Number++; 
        return s_Number; 
    } 
    
  • +0

    このソリューションは、このクラス、ありがとうございました。 –

    +0

    シナリオの@Lasse +1。失われたシナリオについて説明できますか?私が取っている行は:s_Numberはスタック上の値なので、2回インクリメントされていれば増分を失うことはできません。 – bytefire

    +0

    大丈夫です。私はすべてのスレッドが独自のスタックを取得し、値の型がコピーされるのを忘れていました。 – bytefire

    8

    はい、ここでのシナリオである:

    s_number ++

    s_number = 1

    Thread Bを行う行う= 0

    Thread A s_number s_number ++

    s_number = 2

    Thread A両方のスレッドが2


    を返さreturn s_number

    を行うので、あなたはこのようなロック機構を実装する必要がありreturn s_number

    Thread Bの操作を行います。ロック機構

    class Counter 
    { 
        private static int s_Number = 0; 
        private static object _locker = new object(); 
        public static int GetNextNumber() 
        { 
         //Critical section 
         return Interlocked.Increment(ref s_Number); 
        } 
    } 
    

    をします複数のスレッドがyに入るのを防ぐ私たちのクリティカルセクションは同時に。単純な増分よりも多くの操作がある場合は、代わりにLockブロックを使用してください。

    編集:Lasse V. Karlsenによって、より低レベルの動作を説明する、より詳細な答えが書かれています。

    +1

    +1、この状況では、私はおそらく['Interlocked.Increment'](http://msdn.microsoft.com/en-us/library/system.threading.interlocked.increment.aspx)を完全な 'ロック'。 – LukeH

    +0

    @ルークマーリン、ありがとう。しかし私は問題を解決する方法を知っています。私が興味を持っているのはその理由です。 – bytefire

    +1

    @bytefireはこれのためにミニシナリオを追加しました –

    0

    return Interlocked.Increment(ref s_Number);

    これはこれを行います。ロックを使用するよりもはるかに簡単です。ロックブロックは、主にコードブロックに使用する必要があります。

    1

    2つのスレッドが、我々はILの下

    class Counter 
    { 
        private static int s_Number = 0; 
        public static int GetNextNumber() 
        { 
         s_Number++; 
         return s_Number; 
        } 
    } 
    

    ここにあなたのクラスのメソッド用に生成さIL codeを見れば、同時にGetNextNumberメソッドにアクセスしようとすると、同じ番号を取得することが可能である理由は簡単ですコードが生成されています。ご覧のとおり、s_number++は実際には3つの別々の命令で構成され、2つのスレッドが同時にアクセスして同じ初期値を得ることができます。

    Counter.GetNextNumber: 
    IL_0000: ldsfld  UserQuery+Counter.s_Number 
    IL_0005: ldc.i4.1  
    IL_0006: add   
    IL_0007: stsfld  UserQuery+Counter.s_Number 
    IL_000C: ldsfld  UserQuery+Counter.s_Number 
    IL_0011: ret   
    

    これは、両方のスレッド

    同じ値にthread Aをもたらすシナリオが入り、s_Number(IL_0000)の値を取得し、それは値1をロードしますが、この時点で、プロセッサはthread Aを一時停止し、thread Bを開始します。もちろん、s_numberのために定義されたメモリ位置に格納された値はまだ0であり、スレッドBはスレッドAによって使用されたのと同じ値で開始する.1に戻る。スレッドAが再開すると、レジスタは中断時と同様に復元され、それは、0に1を加算し、スレッドBの同じ結果を取り戻す

    このクラスは、並行

    class CounterLocked 
    { 
    private static object o; 
    private static int s_Number = 0; 
    public static int GetNextNumber() 
    { 
        lock(o) 
        { 
        s_Number++; 
        return s_Number; 
        } 
    } 
    } 
    
    CounterLocked.GetNextNumber: 
    IL_0000: ldc.i4.0  
    IL_0001: stloc.0  // <>s__LockTaken0 
    IL_0002: ldsfld  UserQuery+CounterLocked.o 
    IL_0007: dup   
    IL_0008: stloc.2  // CS$2$0001 
    IL_0009: ldloca.s 00 // <>s__LockTaken0 
    IL_000B: call  System.Threading.Monitor.Enter 
    IL_0010: ldsfld  UserQuery+CounterLocked.s_Number 
    IL_0015: ldc.i4.1  
    IL_0016: add   
    IL_0017: stsfld  UserQuery+CounterLocked.s_Number 
    IL_001C: ldsfld  UserQuery+CounterLocked.s_Number 
    IL_0021: stloc.1  // CS$1$0000 
    IL_0022: leave.s  IL_002E 
    IL_0024: ldloc.0  // <>s__LockTaken0 
    IL_0025: brfalse.s IL_002D 
    IL_0027: ldloc.2  // CS$2$0001 
    IL_0028: call  System.Threading.Monitor.Exit 
    IL_002D: endfinally 
    IL_002E: ldloc.1  // CS$1$0000 
    IL_002F: ret   
    

    InterlockIncrementために生成されたコードは非常に簡単であるにブロックするロックキーワードを使用

    public static int GetNextNumber() 
    { 
        return Interlocked.Increment(ref s_Number); 
    } 
    
    CounterLocked.GetNextNumber: 
    IL_0000: ldsflda  UserQuery+CounterLocked.s_Number 
    IL_0005: call  System.Threading.Interlocked.Increment 
    IL_000A: ret 
    
    +0

    それは非常に有用です。私の質問の新しいEDITセクションをご覧ください。 – bytefire

    関連する問題