假设一个类有一个公共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.

其他回答

我做了一些测试来看看这个理论是如何工作的: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年乔·达菲的博客快照)

编辑:正如评论中提到的,这些天我很高兴在单个变量的情况下使用Interlocked,这显然是可以的。当它变得更复杂时,我仍然会恢复到锁定…

当你需要增量操作时,使用volatile是没有用的——因为读和写是分开的指令。另一个线程可以在读取后但在回写之前更改值。

就我个人而言,我几乎总是锁定——以一种明显正确的方式获得正确比波动性或interlocking . increment更容易。就我而言,无锁多线程是为真正的线程专家准备的,而我不是。如果Joe Duffy和他的团队构建了很好的库,可以并行化,而不像我构建的那样有太多的锁,那就太棒了,我马上就会使用它——但是当我自己做线程时,我会尽量保持简单。

您要寻找的是锁定增量或连锁增量。

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和lock之间的区别:

volatile关键字可以应用于以下类型的字段:

引用类型。 指针类型(在不安全的上下文中)。注意,虽然指针本身可以是volatile的,但它所指向的对象却不能。在其他 总之,你不能声明一个“指针”为“volatile”。 简单类型,如sbyte、byte、short、ushort、int、uint、char、float和bool。 具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int或uint。 泛型类型参数称为引用类型。 IntPtr和UIntPtr。

其他类型,包括double和long,不能标记为“volatile” 因为不能保证对这些类型的字段的读写 成为原子。来保护对这些类型的多线程访问 字段,使用互锁类成员或使用 锁的声明。