当前位置:编程学习 > C#/ASP.NET >>

一道关于volatile的难题(涉及到多核和单核)

    class ThreadSyncDemo
    {
        static volatile int sum = 0;  // 静态变量
        static void foo1()
        {
            for (int i = 0; i < 1000000000; ++i)     // 10亿次
            {
                sum++;
            }
            Console.WriteLine("foo1() completed, sum  is " + sum);
        }
        static void foo2()
        {
            for (int i = 0; i < 1000000000; ++i)  // 10亿次
            {
                sum++; 
            }
            Console.WriteLine("foo2() completed, sum  is " + sum);
        }
        // 主函数
        static void Main()
        {

            Thread t1 = new Thread(foo1);
            t1.Start();
            Thread t2 = new Thread(foo2);
            t2.Start();
            Console.ReadLine();
        }
    }

  大家在自己的电脑上编译运行上面的代码,最后结果是否是20亿???(我的CPU是T9300 双核)
  
  foo1() completed, sum  is 1008290573
  foo2() completed, sum  is 1037423262
  请按任意键继续. . .

  能否说说原因。。。 --------------------编程问答-------------------- 我的结果是
foo1() completed, sum is 1971071770
foo2() completed, sum is 2000000000
--------------------编程问答-------------------- 你想做什么?
引用 MSDN:
volatile 关键字指示一个字段可以由多个同时执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可以确保该字段在任何时间呈现的都是最新的值。

这不很正常吗?结果显然并不是自加10亿次,大家都在往上加...你多开几个线程加得更多... --------------------编程问答-------------------- foo1() completed, sum is 1021431908
foo2() completed, sum is 1235668282 --------------------编程问答-------------------- 用join 或者lock都可以得到正确数据 --------------------编程问答-------------------- 同疑惑,大概的意思知道,就是说在访问的时候,有volatile标记的是读取内存,没volatile标记的读取寄存器缓存的值,实际中什么情况是明显的这个关键字的用途所在呢?

^_^

跟楼主一起等答案。 --------------------编程问答-------------------- 线程的执行是乱序的,所以你的foo1和foo2不可能按着顺序完成,也就是说结果20亿相当于foo1和foo2必须全部完成再打印才可能,如果foo1先完成的时候,foo2可能只加了100,结果就回事1000000100,即那十亿的累加加上目前另一个线程完成的100的累加,这样结果就达不到20亿,从你的结果看,你,你的两个线程的确都按照你的本意正常运行了. --------------------编程问答-------------------- 测试了代码,按照你的写法,表达的应该是,至少一个结果应该是20亿,你用volatile就是为了从虚拟内存中并行读取一个值.
但是,CLI使用的是纯堆栈的用,所以就算使用vol关键字,你的sum还是得先入栈,就因为这一步,你foo1刚入栈,切换到foo2运行,也入栈并完成计算,这样foo1,foo2假使读取的都是1000,经过foo2运算sum是1001,但是你foo1接着刚才运算还是1001,这样虽然foo1和2都对sum加1,最后内存里的sum不是1002,依旧是1001,这就是为啥有时候达不到20亿的原因.
--------------------编程问答-------------------- 哦,昨天没仔细看...你想得到刚好20亿的结果?那是不可能的...内存本身就有延迟,硬件架构就有限制... --------------------编程问答-------------------- 实际上,我也很疑惑,volatile到底什么情况下用的到。 --------------------编程问答-------------------- 应该是多处理器使用各自局部缓存造成不同步造成的。

可以在Hyper-Threading单处理器上运行验证下。 --------------------编程问答--------------------
引用 9 楼 wuyazhe 的回复:
实际上,我也很疑惑,volatile到底什么情况下用的到。

用处是...

可以从多个线程安全地访问“只需要执行单个原子操作就能修改”的成员,而不需要使用正式的线程同步技术...

也就是说...为原子操作提供一种简易而又安全的多线程访问方法... --------------------编程问答-------------------- 很感谢大家对这个问题感兴趣,实际上我在很多电脑上运行过这个程序,如果是单核的CPU,最后输出的结果一定是20亿,如果是多核的CPU,结果一般小于20亿。当然,如果加上lock之后,那结果是肯定是等于20亿。我想知道的到底单核和多核CPU在处理volatile问题上的区别而已。 --------------------编程问答--------------------
引用 12 楼 hehuaiwen 的回复:
很感谢大家对这个问题感兴趣,实际上我在很多电脑上运行过这个程序,如果是单核的CPU,最后输出的结果一定是20亿,如果是多核的CPU,结果一般小于20亿。当然,如果加上lock之后,那结果是肯定是等于20亿。我想知道的到底单核和多核CPU在处理volatile问题上的区别而已。

你可以找一个Atom或者单个P4 Xeon(2.0~3.8)或者P4 C/E/3.06B/524 电脑来试。

这种CPU具备超线程技术,有2个逻辑CPU,有2组寄存器,但是只有1组缓存。
如果和单线程一样,是2亿,就验证了我的观点。 --------------------编程问答--------------------
引用 12 楼 hehuaiwen 的回复:
很感谢大家对这个问题感兴趣,实际上我在很多电脑上运行过这个程序,如果是单核的CPU,最后输出的结果一定是20亿,如果是多核的CPU,结果一般小于20亿。当然,如果加上lock之后,那结果是肯定是等于20亿。我想知道的到底单核和多核CPU在处理volatile问题上的区别而已。

首先你要明白,每CPU/Core任何一个指令周期只可能有一个线程在执行...所以单CPU/Core最后输出的结果一定是20亿,因为线程是轮流执行的不会争用...

这个问题应该和超线程无关...超线程只是模拟多线程并行,本质上并没改变...当两个线程同时争用一个资源时,其中一个线程要暂时停止并让出资源,直到资源闲置后才能继续... --------------------编程问答-------------------- 这也是为什么MSDN上说...volatile字段适用于“只需要执行单个原子操作就能修改”的资源,同时还说...
引用 MSDN:
但是,如果此数据成员是类、结构或数组,那么,从多个线程访问它可能会导致间歇的数据损坏。假设有一个更改数组中的值的线程。Windows 会定期中断线程,以便允许其他线程执行。因此,此线程会在分配某些数组元素之后和分配其他元素之前被停止。由于数组现在有了一个程序员从不想要的状态,因此,读取此数组的另一个线程可能会失败。
--------------------编程问答-------------------- 不会用这个。。。。 --------------------编程问答-------------------- 因为sum++不是原子操作。

具体解释见下贴2楼:
http://topic.csdn.net/u/20080821/19/C522CAC6-89C7-434A-92C5-390B4289C889.html --------------------编程问答--------------------
引用 17 楼 gomoku 的回复:
因为sum++不是原子操作。

具体解释见下贴2楼:
http://topic.csdn.net/u/20080821/19/C522CAC6-89C7-434A-92C5-390B4289C889.html


我电脑上运行不是总为那个结果。 --------------------编程问答-------------------- 既然大家都在谈理论,那我有空试验下。 --------------------编程问答-------------------- --------------------编程问答--------------------
引用 18 楼 wuyazhe 的回复:
引用 17 楼 gomoku 的回复:
因为sum++不是原子操作。

具体解释见下贴2楼:
http://topic.csdn.net/u/20080821/19/C522CAC6-89C7-434A-92C5-390B4289C889.html

我电脑上运行不是总为那个结果。

要是总是那个结果程序员就轻松了:)
竞态(racing)是最难重现和调试的。

另:严格的说,单核的机器也会出现上述问题(如果线程切换正好发生在特殊点上)。不过这种运气实在是太稀少了。 --------------------编程问答--------------------
引用 19 楼 caozhy 的回复:
既然大家都在谈理论,那我有空试验下。

嗯,支持试验...超线程那部分我是估计的...我这没法验证,找不到单核计算机了... --------------------编程问答--------------------
引用 22 楼 vrhero 的回复:
引用 19 楼 caozhy 的回复:
既然大家都在谈理论,那我有空试验下。

嗯,支持试验...超线程那部分我是估计的...我这没法验证,找不到单核计算机了...

我回家给你结果去。04年笔记本。单核… --------------------编程问答-------------------- 1楼就是在家测试的了。 --------------------编程问答-------------------- --------------------编程问答-------------------- to 9 楼
实际上,我也很疑惑,volatile到底什么情况下用的到。

struct SpinLock
{
    private volatile int token=0;

    public void Enter()
    {
        while(Interlocked.Exchange(ref token, 1)!=0) // spin
    }

    public void Exit()
    {
       token=0;
    }
} --------------------编程问答-------------------- 我来给答案。
sum++等同于sun=sum+1,既要先读后加最后赋值三步操作。
volatile的作用是不保证原子性也不保证顺序性,而是把数据放入内存中,故两函数读该内存数据时不能保证值为最新值。
例:内存数据值为10,函1读取10,函数2加1后为11,此时内存为11,同时函1还是10,相加后又将数据赋给内存11.
补充:.NET技术 ,  C#
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,