本地变量上存在冲突的线程

本文关键字:冲突 线程 存在 变量 | 更新日期: 2023-09-27 18:26:53

为什么在下面的代码中,n最终不是0,它是一个每次大小小于1000000的随机数,有时甚至是负数?

static void Main(string[] args)
{
    int n = 0;
    var up = new Thread(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                n++;
            }
        });
    up.Start();
    for (int i = 0; i < 1000000; i++)
    {
        n--;
    }
    up.Join();
    Console.WriteLine(n);
    Console.ReadLine();
}

不起来。Join()是否强制两个for循环在调用WriteLine之前完成?

我知道局部变量实际上是幕后类的一部分(认为它被称为闭包),然而,因为局部变量n实际上是堆分配的,这会影响n每次不为0吗?

本地变量上存在冲突的线程

n++n--操作不能保证是原子操作。每个操作有三个阶段:

  1. 从内存读取当前值
  2. 修改值(递增/递减)
  3. 将值写入内存

由于您的两个线程都在重复执行此操作,并且您无法控制线程的调度,因此会出现以下情况:

  • 线程1:获取n(值=0)
  • 线程1:增量(值=1)
  • Thread2:获取n(值=0)
  • 线程1:写入n(n=1)
  • 线程2:递减(值=-1)
  • 线程1:获取n(值=1)
  • 线程2:写入n(n=-1)

等等。

这就是为什么锁定对共享数据的访问总是很重要的原因。

--代码:

static void Main(string[] args)
{
    int n = 0;
    object lck = new object();
    var up = new Thread(() => 
        {
            for (int i = 0; i < 1000000; i++)
            {
                lock (lck)
                    n++;
            }
        });
    up.Start();
    for (int i = 0; i < 1000000; i++)
    {
        lock (lck)
            n--;
    }
    up.Join();
    Console.WriteLine(n);
    Console.ReadLine();
}

--编辑:关于lock如何工作的更多信息。。。

当您使用lock语句时,它会尝试获取您提供给它的对象的锁,即我上面代码中的lck对象。如果该对象已经锁定,lock语句将导致代码等待锁定释放后再继续。

C#lock语句实际上与Critical Section相同。实际上,它类似于以下C++代码:

// declare and initialize the critical section (analog to 'object lck' in code above)
CRITICAL_SECTION lck;
InitializeCriticalSection(&lck);
// Lock critical section (same as 'lock (lck) { ...code... }')
EnterCriticalSection(&lck);
__try
{
    // '...code...' goes here
    n++;
}
__finally
{
    LeaveCriticalSection(&lck);
}

C#lock语句将大部分内容抽象掉,这意味着我们更难进入关键部分(获取锁)并忘记离开它

不过,重要的是,只有您的锁定对象会受到影响,并且仅针对试图获取同一对象上的锁定的其他线程。没有什么可以阻止您编写代码来修改锁定对象本身,或访问任何其他对象YOU负责确保您的代码尊重锁,并在写入共享对象时始终获取锁。

否则,你会得到一个不确定的结果,就像你在这段代码中看到的那样,或者规范编写者喜欢称之为"未定义的行为"。龙来了(以虫子的形式,你会遇到无尽的麻烦)。

是的,up.Join()将确保两个循环都在调用WriteLine之前结束。

然而,实际情况是这两个循环同时执行,每个循环都在自己的线程中。

两个线程之间的切换始终由操作系统完成,每次运行的程序都会显示不同的切换定时集。

您还应该注意,n--n++不是原子操作,并且实际上被编译为3个子操作,例如:

Take value from memory
Increase it by one
Put value in memory

最后一个难题是,线程上下文切换可以在n++n--内部,在上述3个操作中的任何一个操作之间发生。

这就是为什么最终值是不确定的。

如果不想使用锁,Interlocked类中有递增和递减运算器的原子版本。

将您的代码更改为以下内容,您将始终获得0的答案。

static void Main(string[] args)
{
    int n = 0;
    var up = new Thread(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                Interlocked.Increment(ref n);
            }
        });
    up.Start();
    for (int i = 0; i < 1000000; i++)
    {
        Interlocked.Decrement(ref n);
    }
    up.Join();
    Console.WriteLine(n);
    Console.ReadLine();
}

您需要更早地加入线程:

static void Main(string[] args)
    {
        int n = 0;
        var up = new Thread(() =>
        {
            for (int i = 0; i < 1000000; i++)
            {
                n++;
            }
        });
        up.Start();
        up.Join();
        for (int i = 0; i < 1000000; i++)
        {
            n--;
        }

        Console.WriteLine(n);
        Console.ReadLine();
    }