局部变量上的线程冲突

时间:2013-08-09 05:05:16

标签: c#

为什么,在下面的代码中,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()强制两个for循环在调用WriteLine之前完成?

我理解局部变量实际上是幕后类的一部分(认为它叫做闭包),但是因为局部变量n实际上是堆分配的,那么每次会影响n不为0吗?

4 个答案:

答案 0 :(得分:7)

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

  1. 从内存中读取当前值
  2. 修改值(递增/递减)
  3. 将值写入内存
  4. 由于你的两个线程都反复这样做,并且你无法控制线程的调度,你会遇到这样的情况:

    • 线程1:获取n(值= 0)
    • 线程1:增量(值= 1)
    • 线程2:获取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语句抽象了大部分内容,这意味着我们进入一个关键部分(获得锁定)并且忘记离开它会更加困难。

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

    否则,你将会看到一个非确定性结果,就像你已经看到过这段代码,或者规范编写者喜欢称之为'未定义行为'。这里是龙(以虫子的形式,你会遇到无穷无尽的麻烦)。

答案 1 :(得分:6)

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

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

两个线程之间的切换由操作系统一直进行,每个程序运行将显示不同的切换时序设置。

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

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

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

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

答案 2 :(得分: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();
}

答案 3 :(得分:-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();
    }