假设一个类有一个公共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的使用。但有理由不这么做吗?
当前回答
您要寻找的是锁定增量或连锁增量。
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绝对不是你想要的——它只是告诉编译器将变量视为总是在变化,即使当前代码路径允许编译器优化内存读取。
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是有用的。这是一个与增量正交的选择。
如果您想了解更多关于无锁代码的内容,以及编写无锁代码的正确方法,这是一篇非常好的文章
http://www.ddj.com/hpc-high-performance-computing/210604448
编辑:正如评论中提到的,这些天我很高兴在单个变量的情况下使用Interlocked,这显然是可以的。当它变得更复杂时,我仍然会恢复到锁定…
当你需要增量操作时,使用volatile是没有用的——因为读和写是分开的指令。另一个线程可以在读取后但在回写之前更改值。
就我个人而言,我几乎总是锁定——以一种明显正确的方式获得正确比波动性或interlocking . increment更容易。就我而言,无锁多线程是为真正的线程专家准备的,而我不是。如果Joe Duffy和他的团队构建了很好的库,可以并行化,而不像我构建的那样有太多的锁,那就太棒了,我马上就会使用它——但是当我自己做线程时,我会尽量保持简单。
我想补充一下其他答案中提到的volatile、Interlocked和lock之间的区别:
volatile关键字可以应用于以下类型的字段:
引用类型。 指针类型(在不安全的上下文中)。注意,虽然指针本身可以是volatile的,但它所指向的对象却不能。在其他 总之,你不能声明一个“指针”为“volatile”。 简单类型,如sbyte、byte、short、ushort、int、uint、char、float和bool。 具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int或uint。 泛型类型参数称为引用类型。 IntPtr和UIntPtr。
其他类型,包括double和long,不能标记为“volatile” 因为不能保证对这些类型的字段的读写 成为原子。来保护对这些类型的多线程访问 字段,使用互锁类成员或使用 锁的声明。
Lock(…)可以工作,但可能阻塞线程,如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。
联锁。正确的做法是……更少的开销,因为现代cpu支持这个原语。
Volatile本身是不正确的。试图检索并写回修改后的值的线程仍然可能与执行相同操作的另一个线程发生冲突。