假设一个类有一个公共int计数器字段,可以被多个线程访问。这个int值只能自增或自减。
要增加这个字段,应该使用哪种方法,为什么?
锁(this.locker) this.counter + +; 联锁。增量(ref this.counter); 将counter的访问修饰符更改为public volatile。
现在我已经发现了volatile,我已经删除了许多lock语句和Interlocked的使用。但有理由不这么做吗?
假设一个类有一个公共int计数器字段,可以被多个线程访问。这个int值只能自增或自减。
要增加这个字段,应该使用哪种方法,为什么?
锁(this.locker) this.counter + +; 联锁。增量(ref this.counter); 将counter的访问修饰符更改为public volatile。
现在我已经发现了volatile,我已经删除了许多lock语句和Interlocked的使用。但有理由不这么做吗?
当前回答
Lock(…)可以工作,但可能阻塞线程,如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。
联锁。正确的做法是……更少的开销,因为现代cpu支持这个原语。
Volatile本身是不正确的。试图检索并写回修改后的值的线程仍然可能与执行相同操作的另一个线程发生冲突。
其他回答
我只是在这里指出Orion Edwards回答中关于volatile的错误。
他说:
“如果它是不稳定的,这只是确保两个cpu看到相同的数据 同样的时间。”
这是错误的。在微软关于volatile的文档中,提到:
在多处理器系统上,volatile读操作不会 保证获得写入该内存位置的最新值 任何处理器。类似地,volatile写操作不会 保证写入的值对其他人立即可见 处理器。”
我做了一些测试来看看这个理论是如何工作的:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。我的测试更侧重于compareexchange,但Increment的结果是类似的。在多cpu环境下,联锁并不需要更快。下面是在2年的16 CPU服务器上对Increment的测试结果。请记住,测试还包括增加后的安全读取,这在现实世界中是典型的。
D:\>InterlockVsMonitor.exe 16
Using 16 threads:
InterlockAtomic.RunIncrement (ns): 8355 Average, 8302 Minimal, 8409 Maxmial
MonitorVolatileAtomic.RunIncrement (ns): 7077 Average, 6843 Minimal, 7243 Maxmial
D:\>InterlockVsMonitor.exe 4
Using 4 threads:
InterlockAtomic.RunIncrement (ns): 4319 Average, 4319 Minimal, 4321 Maxmial
MonitorVolatileAtomic.RunIncrement (ns): 933 Average, 802 Minimal, 1018 Maxmial
我赞同Jon Skeet的回答,并想为所有想了解“volatile”和“Interlocked”的人添加以下链接:
原子性,波动性和不可变性是不同的,第一部分- (Eric Lippert的神话般的冒险在编码)
原子性、挥发性和不变性是不同的,第二部分
原子性、挥发性和不变性是不同的,第三部分
Sayonara Volatile - (Wayback Machine 2012年乔·达菲的博客快照)
您要寻找的是锁定增量或连锁增量。
Volatile绝对不是你想要的——它只是告诉编译器将变量视为总是在变化,即使当前代码路径允许编译器优化内存读取。
e.g.
while (m_Var)
{ }
if m_Var is set to false in another thread but it's not declared as volatile, the compiler is free to make it an infinite loop (but doesn't mean it always will) by making it check against a CPU register (e.g. EAX because that was what m_Var was fetched into from the very beginning) instead of issuing another read to the memory location of m_Var (this may be cached - we don't know and don't care and that's the point of cache coherency of x86/x64). All the posts earlier by others who mentioned instruction reordering simply show they don't understand x86/x64 architectures. Volatile does not issue read/write barriers as implied by the earlier posts saying 'it prevents reordering'. In fact, thanks again to MESI protocol, we are guaranteed the result we read is always the same across CPUs regardless of whether the actual results have been retired to physical memory or simply reside in the local CPU's cache. I won't go too far into the details of this but rest assured that if this goes wrong, Intel/AMD would likely issue a processor recall! This also means that we do not have to care about out of order execution etc. Results are always guaranteed to retire in order - otherwise we are stuffed!
使用Interlocked Increment,处理器需要出去,从给定的地址获取值,然后增加并将其写回来——所有这些都是在拥有整个缓存线的独占所有权(锁定xadd)的情况下进行的,以确保没有其他处理器可以修改它的值。
使用volatile,你仍然会得到一条指令(假设JIT是有效的)——inc dword ptr [m_Var]。然而,处理器(cpuA)在执行与联锁版本相同的操作时,并不要求独占缓存线的所有权。正如您可以想象的那样,这意味着其他处理器可以在cpuA读取更新后的值后将其写回m_Var。所以现在不是增加了两次,而是只增加了一次。
希望这能解决问题。
有关更多信息,请参见“了解多线程应用程序中低锁技术的影响”- http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
附注:是什么原因导致这么晚才回复?在他们的解释中,所有的回答都是如此明显的错误(尤其是被标记为答案的那个),我不得不为其他阅读这篇文章的人澄清一下。耸了耸肩
p.p.s. I'm assuming that the target is x86/x64 and not IA64 (it has a different memory model). Note that Microsoft's ECMA specs is screwed up in that it specifies the weakest memory model instead of the strongest one (it's always better to specify against the strongest memory model so it is consistent across platforms - otherwise code that would run 24-7 on x86/x64 may not run at all on IA64 although Intel has implemented similarly strong memory model for IA64) - Microsoft admitted this themselves - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.
“volatile”不替换interlocked。它只是确保变量没有被缓存,而是直接使用。
增加一个变量实际上需要三个操作:
读 增量 写
联锁。Increment作为单个原子操作执行所有三个部分。