7

为什么呢,在下面的代码中,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();
}

up.Join() 不会在调用 WriteLine 之前强制两个 for 循环完成吗?

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

4

4 回答 4

7

和操作不保证是原子的n++n--每个操作分为三个阶段:

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

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

  • 线程 1:获取n(值 = 0)
  • Thread1:增量(值 = 1)
  • 线程 2:获取n(值 = 0)
  • 线程1:写n(n == 1)
  • Thread2:递减(值 = -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语句将大部分内容抽象出来,这意味着我们更难进入临界区(获取锁)而忘记离开它。

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

否则,您将得到一个不确定的结果,就像您在这段代码中看到的那样,或者规范编写者喜欢称之为“未定义的行为”。Here Be Dragons(以错误的形式出现,您将遇到无穷无尽的麻烦)。

于 2013-08-09T05:15:07.193 回答
6

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

但是,正在发生的事情是两个循环同时执行,每个循环都在它自己的线程中。

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

您还应该知道n--andn++不是原子操作,实际上正在编译为 3 个子操作,例如:

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

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

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

于 2013-08-09T05:13:38.447 回答
2

如果您不想使用锁,则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();
}
于 2013-08-09T06:59:21.163 回答
-2

您需要提前加入线程:

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();
    }
于 2013-08-09T05:16:05.183 回答