假设一个类有一个公共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是有用的。这是一个与增量正交的选择。
如果您想了解更多关于无锁代码的内容,以及编写无锁代码的正确方法,这是一篇非常好的文章
http://www.ddj.com/hpc-high-performance-computing/210604448
编辑:正如评论中提到的,这些天我很高兴在单个变量的情况下使用Interlocked,这显然是可以的。当它变得更复杂时,我仍然会恢复到锁定…
当你需要增量操作时,使用volatile是没有用的——因为读和写是分开的指令。另一个线程可以在读取后但在回写之前更改值。
就我个人而言,我几乎总是锁定——以一种明显正确的方式获得正确比波动性或interlocking . increment更容易。就我而言,无锁多线程是为真正的线程专家准备的,而我不是。如果Joe Duffy和他的团队构建了很好的库,可以并行化,而不像我构建的那样有太多的锁,那就太棒了,我马上就会使用它——但是当我自己做线程时,我会尽量保持简单。
Lock(…)可以工作,但可能阻塞线程,如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。
联锁。正确的做法是……更少的开销,因为现代cpu支持这个原语。
Volatile本身是不正确的。试图检索并写回修改后的值的线程仍然可能与执行相同操作的另一个线程发生冲突。
“volatile”不替换interlocked。它只是确保变量没有被缓存,而是直接使用。
增加一个变量实际上需要三个操作:
读 增量 写
联锁。Increment作为单个原子操作执行所有三个部分。
最坏的情况(实际上不起作用)
将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。
我做了一些测试来看看这个理论是如何工作的: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
您要寻找的是锁定增量或连锁增量。
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.
我赞同Jon Skeet的回答,并想为所有想了解“volatile”和“Interlocked”的人添加以下链接:
原子性,波动性和不可变性是不同的,第一部分- (Eric Lippert的神话般的冒险在编码)
原子性、挥发性和不变性是不同的,第二部分
原子性、挥发性和不变性是不同的,第三部分
Sayonara Volatile - (Wayback Machine 2012年乔·达菲的博客快照)
我想补充一下其他答案中提到的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” 因为不能保证对这些类型的字段的读写 成为原子。来保护对这些类型的多线程访问 字段,使用互锁类成员或使用 锁的声明。
我只是在这里指出Orion Edwards回答中关于volatile的错误。
他说:
“如果它是不稳定的,这只是确保两个cpu看到相同的数据 同样的时间。”
这是错误的。在微软关于volatile的文档中,提到:
在多处理器系统上,volatile读操作不会 保证获得写入该内存位置的最新值 任何处理器。类似地,volatile写操作不会 保证写入的值对其他人立即可见 处理器。”