一道关于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
--------------------编程问答-------------------- 你想做什么?
这不很正常吗?结果显然并不是自加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单处理器上运行验证下。 --------------------编程问答--------------------
用处是...
可以从多个线程安全地访问“只需要执行单个原子操作就能修改”的成员,而不需要使用正式的线程同步技术...
也就是说...为原子操作提供一种简易而又安全的多线程访问方法... --------------------编程问答-------------------- 很感谢大家对这个问题感兴趣,实际上我在很多电脑上运行过这个程序,如果是单核的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亿,就验证了我的观点。 --------------------编程问答--------------------
首先你要明白,每CPU/Core任何一个指令周期只可能有一个线程在执行...所以单CPU/Core最后输出的结果一定是20亿,因为线程是轮流执行的不会争用...
这个问题应该和超线程无关...超线程只是模拟多线程并行,本质上并没改变...当两个线程同时争用一个资源时,其中一个线程要暂时停止并让出资源,直到资源闲置后才能继续... --------------------编程问答-------------------- 这也是为什么MSDN上说...volatile字段适用于“只需要执行单个原子操作就能修改”的资源,同时还说...
--------------------编程问答-------------------- 不会用这个。。。。 --------------------编程问答-------------------- 因为sum++不是原子操作。
具体解释见下贴2楼:
http://topic.csdn.net/u/20080821/19/C522CAC6-89C7-434A-92C5-390B4289C889.html --------------------编程问答--------------------
我电脑上运行不是总为那个结果。 --------------------编程问答-------------------- 既然大家都在谈理论,那我有空试验下。 --------------------编程问答-------------------- --------------------编程问答--------------------
要是总是那个结果程序员就轻松了:)
竞态(racing)是最难重现和调试的。
另:严格的说,单核的机器也会出现上述问题(如果线程切换正好发生在特殊点上)。不过这种运气实在是太稀少了。 --------------------编程问答--------------------
嗯,支持试验...超线程那部分我是估计的...我这没法验证,找不到单核计算机了... --------------------编程问答--------------------
我回家给你结果去。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#