使用静态变量的并发性
本文关键字:并发 变量 静态 | 更新日期: 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);
}
这将确保每个线程将返回一个唯一的值(只要这个数字没有溢出),并且不会丢失增量。
由于原始代码可以分成以下步骤:
- 读取
s_Number
的现有值 - 添加1
- 将新值存储到
s_Number
- Read
s_Number
- 返回读取值
可能发生的情况有:
- 两个线程在其余线程之前执行步骤1,这意味着两个线程将读取相同的现有值,增加1,最终得到相同的值。增量丢失
- 线程可以执行步骤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简单得多。锁块应该主要用于代码块,通常为