假设一个类有一个公共int计数器字段,可以被多个线程访问。这个int值只能自增或自减。

要增加这个字段,应该使用哪种方法,为什么?

锁(this.locker) this.counter + +; 联锁。增量(ref this.counter); 将counter的访问修饰符更改为public volatile。

现在我已经发现了volatile,我已经删除了许多lock语句和Interlocked的使用。但有理由不这么做吗?


当前回答

我赞同Jon Skeet的回答,并想为所有想了解“volatile”和“Interlocked”的人添加以下链接:

原子性,波动性和不可变性是不同的,第一部分- (Eric Lippert的神话般的冒险在编码)

原子性、挥发性和不变性是不同的,第二部分

原子性、挥发性和不变性是不同的,第三部分

Sayonara Volatile - (Wayback Machine 2012年乔·达菲的博客快照)

其他回答

最坏的情况(实际上不起作用)

将counter的访问修饰符更改为public volatile

正如其他人提到的,这本身一点也不安全。volatile的关键在于在多个cpu上运行的多个线程可以缓存数据并重新排序指令。

如果它不是易失性的,并且CPU A增加了一个值,那么CPU B可能直到一段时间后才真正看到这个增加的值,这可能会导致问题。

如果它是volatile,这只是确保两个cpu同时看到相同的数据。它根本不会阻止它们的读和写操作交叉,而这正是您试图避免的问题。

第二个最好的:

锁(this.locker) this.counter++;

这样做是安全的(前提是您记得锁定访问此.counter的所有其他地方)。它阻止任何其他线程执行任何其他由locker保护的代码。 使用锁还可以防止如上所述的多cpu重新排序问题,这是非常棒的。

问题是,锁的速度很慢,如果你在其他一些不相关的地方重用了这个锁,那么你可能会无缘无故地阻塞其他线程。

Best

联锁。增量(ref this.counter);

这是安全的,因为它有效地在“一次命中”中完成读取、增量和写入,并且不能中断。因此,它不会影响任何其他代码,也不需要记住在其他地方锁定。它也非常快(正如MSDN所说,在现代CPU上,这通常是一个单一的CPU指令)。

但我不完全确定它是否绕过了其他cpu的重排序,或者你是否还需要将volatile与增量结合起来。

联锁笔记:

联锁方法在任何数量的内核或cpu上都是同时安全的。 互锁方法在它们执行的指令周围应用了一个完整的围栏,因此不会发生重新排序。 互锁方法不需要甚至不支持对volatile字段的访问,因为volatile在给定字段的操作周围放置了半个围栏,而互锁方法使用了完整的围栏。

脚注:volatile实际上有什么好处。

既然volatile不能防止这些多线程问题,那么它是用来干什么的呢?一个很好的例子是,你有两个线程,一个总是写入一个变量(比如queueLength),另一个总是从同一个变量读取。

如果queueLength不是易变的,线程A可能会写5次,但线程B可能会认为这些写被延迟了(甚至可能顺序错误)。

一种解决方案是锁定,但在这种情况下也可以使用volatile。这将确保线程B始终能够看到线程A所写的最新内容。但是请注意,这种逻辑只适用于以下情况:作者从不阅读,读者从不写作,并且您正在编写的内容是一个原子值。一旦执行了单个的read-modify-write操作,就需要转到Interlocked操作或使用Lock。

Lock(…)可以工作,但可能阻塞线程,如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。

联锁。正确的做法是……更少的开销,因为现代cpu支持这个原语。

Volatile本身是不正确的。试图检索并写回修改后的值的线程仍然可能与执行相同操作的另一个线程发生冲突。

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

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” 因为不能保证对这些类型的字段的读写 成为原子。来保护对这些类型的多线程访问 字段,使用互锁类成员或使用 锁的声明。

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

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

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