c# - 使用静态变量的并发

标签 c# concurrency static

在下面的代码中,如果 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. 读取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;
}

关于c# - 使用静态变量的并发,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15966903/

相关文章:

c# - 检查C#中的字符串是否为URL

c# - 在 C# 中到处转换( float )。只是 double 工作更好吗?

java - 在读取器/写入器场景中,我是否需要使两个变量都为 volatile ?

compiler-errors - 为什么我不能分配意图?

c# - 可重复使用的 MVC 代码 : handling value types in linq expressions

c# - 自定义 TaskScheduler、SynchronizationContext?

java - 是否有执行死锁检测的 Lock 实现?

amazon-web-services - 使用 Goroutines 和 Channels 将多个文件并行上传到 Amazon S3

c++ - c中的静态初始化

c# - 如何在 C++ 中声明类的静态实例?