在下面的代码中,如果两个线程同时调用GetNextNumber()
,那么它可能会向两个线程返回相同的数字吗?
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
s_Number++;
return s_Number;
}
}
你能解释一下原因吗?
编辑:如果代码可以向两个线程返回相同的数字,那么以下是正确的吗?假设当GetNextNumber()
等于2时,两个线程调用s_Number
。如果返回相同的值,那么该值只能为4.它不能是3.这是正确的吗?
答案 0 :(得分:10)
在处理这样一个简单的计数器时,最好使用Interlocked.Increment
:
private static int s_Number = 0;
public static int GetNextNumber()
{
return Interlocked.Increment(ref s_Number);
}
这将确保每个线程将返回一个唯一值(只要该数字不会溢出),并且不会丢失任何增量。
由于原始代码可以分解为以下步骤:
s_Number
s_Number
s_Number
可能出现的情况是:
对于需要访问 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);
这样做。它比使用锁更简单。锁定块通常应主要用于代码块。