使用静态变量的并发性

本文关键字:并发 变量 静态 | 更新日期: 2023-09-27 18:08:53

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

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

你能解释为什么吗?

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

使用静态变量的并发性

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

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

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

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

  1. 读取s_Number的现有值
  2. 添加1
  3. 将新值存储到s_Number
  4. Read s_Number
  5. 返回读取值

可能发生的情况有:

  1. 两个线程在其余线程之前执行步骤1,这意味着两个线程将读取相同的现有值,增加1,最终得到相同的值。增量丢失
  2. 线程可以执行步骤1到步骤3而不会发生冲突,但是在两个线程都更新了变量并检索了相同的值之后,最终执行步骤4。跳过一个号码

对于较大的代码段,需要自动访问更多的数据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,两个线程都返回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代码,启用优化(右下角的小/0 +),如果您想玩代码,看看它如何转换为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;
}

是的,这是一个场景:

s_number = 0

Thread A do s_number ++

s_number = 1

Thread B do s_number ++

s_number = 2

Thread A do return s_number

Thread B do 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写了一个更深入的答案,解释了更多的低级行为。

当两个线程试图同时访问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         

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

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  

返回互锁。增量(ref s_Number);

这就行了。它比使用lock简单得多。锁块应该主要用于代码块,通常为