5

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

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

你能解释一下为什么吗?

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

4

4 回答 4

12

在处理这样一个简单的计数器时,最好使用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. 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 代码,启用优化(右下角的小 /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;
}
于 2013-04-12T08:58:53.300 回答
8

是的,这是一个场景:

s_number = 0

Thread As_number ++

s_number = 1

Thread Bs_number ++

s_number = 2

Thread Areturn s_number

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

于 2013-04-12T08:54:42.033 回答
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         

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

输入并获取 s_Number (IL_0000)的thread A值,它加载值 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  
于 2013-04-12T08:57:34.900 回答
0

返回 Interlocked.Increment(ref s_Number);

这会做到的。它比使用锁要简单得多。通常,锁定块应该主要用于代码块。

于 2013-04-12T08:57:17.743 回答