使用静态变量的并发

时间:2013-04-12 08:49:28

标签: c# concurrency static

在下面的代码中,如果两个线程同时调用GetNextNumber(),那么它可能会向两个线程返回相同的数字吗?

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

你能解释一下原因吗?

编辑:如果代码可以向两个线程返回相同的数字,那么以下是正确的吗?假设当GetNextNumber()等于2时,两个线程调用s_Number。如果返回相同的值,那么该值只能为4.它不能是3.这是正确的吗?

4 个答案:

答案 0 :(得分:10)

在处理这样一个简单的计数器时,最好使用Interlocked.Increment

private static int s_Number = 0;

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

这将确保每个线程将返回一个唯一值(只要该数字不会溢出),并且不会丢失任何增量。

由于原始代码可以分解为以下步骤:

  1. 阅读s_Number
  2. 的现有值
  3. 添加1
  4. 将新值存储到s_Number
  5. 阅读s_Number
  6. 返回读取值
  7. 可能出现的情况是:

    1. 两个线程在执行其余操作之前执行步骤1,这意味着两个线程将读取相同的现有值,递增1,最终得到相同的值。 失去增量
    2. 线程可以执行步骤1到3而不会发生冲突,但是在两个线程更新了变量并检索相同的值之后,最终执行步骤4。 跳过一个数字
    3. 对于需要访问 atomically 的更多数据的更大代码段,lock语句通常是更好的方法:

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

      但是对于这么简单的一段代码,您需要做的就是以原子方式递增字段并检索新值,Interlocked.Increment更好,更快,代码更少。

      Interlocked类还有其他方法,它们在处理的场景中非常方便。

      丢失增量的更详细说明

      假设s_Number在两个线程执行之前从0开始:

      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,并且两个线程都返回你在这里输了一个增量。

      跳过号码的详细说明

      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级别上的外观。我将原始代码添加到带有注释的IL指令

      // 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
      // }
      

      注意,我使用LINQPad来获取上面的IL代码,启用优化(右下角的小/ o +),如果你想使用代码来看它如何转换成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;
      }
      

答案 1 :(得分:8)

是的,这是一个场景:

s_number = 0

Thread A执行s_number ++

s_number = 1

Thread B执行s_number ++

s_number = 2

Thread A执行return s_number

Thread B执行return s_number

两个线程都返回2。


因此,您应该实现这样的锁定机制:

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);
    }
}

锁定机制将阻止多个线程同时进入您的关键部分。如果您的操作多于简单增量,请改用Lock块。

编辑:Lasse V. Karlsen撰写了一篇更为深入的答案,解释了更多的低级行为。

答案 2 :(得分:1)

如果我们查看为您的类方法生成的GetNextNumber,当两个线程尝试同时访问IL code方法时,很容易理解为什么可以获得相同的数字

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

下面是生成的IL代码,正如您所看到的,s_number++实际上由三个单独的指令组成,这两个指令可以由两个线程同时访问,从而获得相同的初始值。

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         

这是两个线程导致相同值的SCENARIO

thread A输入并获取s_Number(IL_0000)的值,它会加载值1,但此时,处理器暂停thread A并启动thread B。当然,存储在为s_number定义的内存位置的值仍为0,并且线程B以与线程A使用的相同值开始。它返回1.当线程A恢复时,其寄存器将恢复为它们所处的位置暂停时间,因此它将1加0并返回与线程B相同的结果。

此类使用lock关键字来阻止并发

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  

答案 3 :(得分:0)

返回Interlocked.Increment(ref s_Number);

这样做。它比使用锁更简单。锁定块通常应主要用于代码块。