在下面的代码中,如果 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
- 读取
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 代码,启用优化(右下角的小/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;
}
关于c# - 使用静态变量的并发,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15966903/