挥发性VS挥发性联锁对比锁
-
03-07-2019 - |
题
假设一个类有一个 public int counter
由多个线程访问的字段。这 int
仅递增或递减。
要增加此字段,应使用哪种方法,为什么?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- 更改访问修饰符
counter
到public volatile
.
现在我发现了 volatile
, ,我已经删除了很多 lock
声明和使用 Interlocked
. 。但有理由不这样做吗?
解决方案
最差(实际上不起作用)
更改访问修饰符
counter
到public volatile
正如其他人提到的,这本身实际上根本不安全。要点是 volatile
是在多个 CPU 上运行的多个线程可以并且将会缓存数据并重新排序指令。
如果是 不是 volatile
, ,并且 CPU A 递增一个值,那么 CPU B 可能直到一段时间后才真正看到该递增的值,这可能会导致问题。
如果是 volatile
, ,这只是确保两个CPU同时看到相同的数据。它根本不会阻止他们交错读取和写入操作,这是您试图避免的问题。
次好的:
lock(this.locker) this.counter++
;
这样做是安全的(只要您记得 lock
您访问的其他任何地方 this.counter
)。它防止任何其他线程执行由以下代码保护的任何其他代码 locker
。使用锁还可以防止上述多 CPU 重新排序问题,这很棒。
问题是,锁定速度很慢,如果你重复使用 locker
在其他一些并不真正相关的地方,那么您最终可能会无缘无故地阻塞其他线程。
最好的
Interlocked.Increment(ref this.counter);
这是安全的,因为它可以有效地“一次点击”完成读取、增量和写入,并且不能被中断。因此,它不会影响任何其他代码,并且您也不需要记住在其他地方锁定。它也非常快(正如 MSDN 所说,在现代 CPU 上,这通常实际上是一条 CPU 指令)。
不过,我并不完全确定它是否可以绕过其他 CPU 重新排序,或者您是否还需要将 易失性 与增量结合起来。
联锁注释:
- 互锁方法在任意数量的内核或 CPU 上同时都是安全的。
- 互锁方法在它们执行的指令周围应用完整的栅栏,因此不会发生重新排序。
- 联锁方法 不需要甚至不支持访问易失性字段, ,因为易失性在给定字段的操作周围放置了半栅栏,而互锁则使用完整栅栏。
脚注:挥发性实际上有什么好处。
作为 volatile
不能防止此类多线程问题,它有什么用?一个很好的例子是,您有两个线程,其中一个总是写入变量(例如 queueLength
),以及始终从同一个变量读取的变量。
如果 queueLength
不是易失性的,线程 A 可能会写入五次,但线程 B 可能会认为这些写入被延迟(甚至可能以错误的顺序)。
解决方案是锁定,但在这种情况下您也可以使用 volatile。这将确保线程 B 始终看到线程 A 写入的最新内容。但请注意,这个逻辑 仅有的 如果你有从不读书的作家和从不写作的读者, 和 如果你正在写的东西是一个原子值。一旦执行了一次读-修改-写操作,您就需要进行互锁操作或使用锁。
其他提示
编辑: 正如评论中所述,这些天我很高兴使用 Interlocked
对于a的情况 单变量 它在哪里 明显地 好的。当事情变得更复杂时,我仍然会恢复锁定......
使用 volatile
当您需要递增时不会有帮助 - 因为读取和写入是单独的指令。另一个线程可能会在您读取之后但写回之前更改该值。
就我个人而言,我几乎总是只是锁定 - 以某种方式更容易获得正确的结果 明显地 比波动率或 Interlocked.Increment 更正确。就我而言,无锁多线程适用于真正的线程专家,而我不是其中之一。如果 Joe Duffy 和他的团队构建了很好的库,可以并行化事物,而无需像我构建的东西那样多的锁定,那就太棒了,我会立即使用它 - 但是当我自己进行线程处理时,我会尝试把事情简单化。
"volatile
“ 不取代 Interlocked.Increment
!它只是确保变量不被缓存,而是直接使用。
增加一个变量实际上需要三个操作:
- 读
- 增量
- 写
Interlocked.Increment
将所有三个部分作为单个原子操作执行。
您需要的是锁定或互锁增量。
易失性绝对不是您想要的 - 它只是告诉编译器将变量视为始终变化,即使当前代码路径允许编译器优化从内存的读取。
例如
while (m_Var)
{ }
如果 m_Var 在另一个线程中设置为 false 但未声明为 易失性,则编译器可以通过检查 CPU 寄存器(例如EAX 因为这就是 m_Var 从一开始就被获取的内容),而不是对 m_Var 的内存位置发出另一个读取(这可能会被缓存 - 我们不知道也不关心,这就是 x86 缓存一致性的要点/x64)。其他人之前提到指令重新排序的所有帖子都表明他们不了解 x86/x64 架构。挥发性确实 不是 正如之前的帖子所说的“它阻止重新排序”所暗示的那样,出现读/写障碍。事实上,再次感谢 MESI 协议,我们可以保证读取的结果在各个 CPU 上始终相同,无论实际结果是否已退至物理内存或仅驻留在本地 CPU 的缓存中。我不会详细介绍这一点,但请放心,如果出现问题,英特尔/AMD 可能会召回处理器!这也意味着我们不必关心乱序执行等问题。结果总是保证按顺序退休——否则我们就吃饱了!
使用互锁增量,处理器需要出去,从给定的地址获取值,然后增量并将其写回 - 所有这些同时拥有整个缓存行的独占所有权(锁定 xadd)以确保没有其他处理器可以修改它的价值。
使用 volatility,您仍然会得到只有 1 条指令(假设 JIT 是高效的)- inc dword ptr [m_Var]。然而,处理器 (cpuA) 在执行对互锁版本所做的所有操作时并不要求高速缓存行的独占所有权。正如您可以想象的那样,这意味着其他处理器可以在 cpuA 读取更新后的值后将其写回 m_Var。因此,现在您不再将值增加两次,而是只增加一次。
希望这能解决这个问题。
有关详细信息,请参阅“了解多线程应用程序中低锁技术的影响”- http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
附注是什么导致这么晚才回复?所有的回复在他们的解释中都是如此公然不正确(尤其是标记为答案的回复),我只需要为其他阅读本文的人澄清这一点。 耸肩
附注我假设目标是 x86/x64 而不是 IA64(它有不同的内存模型)。请注意,微软的 ECMA 规范被搞砸了,因为它指定了最弱的内存模型而不是最强的内存模型(最好针对最强的内存模型进行指定,这样它在跨平台上是一致的 - 否则代码将在 x86/7 上运行 24-7) x64 可能根本无法在 IA64 上运行,尽管英特尔已经为 IA64 实现了类似的强大内存模型) - 微软自己也承认了这一点 - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.
互锁功能不会锁定。它们是原子的,这意味着它们可以在增量期间完成而无需上下文切换。因此不会出现僵局或等待。
我想说你应该总是更喜欢它而不是锁定和增量。
如果您需要在一个线程中进行写入以在另一个线程中读取,并且您希望优化器不要对变量上的操作重新排序(因为优化器不知道的另一个线程中正在发生的事情),则 Volatile 非常有用。这是与你如何增量的正交选择。
如果您想了解有关无锁代码的更多信息以及编写无锁代码的正确方法,这是一篇非常好的文章
lock(...) 可以工作,但可能会阻塞线程,并且如果其他代码以不兼容的方式使用相同的锁,则可能会导致死锁。
Interlocked.* 是正确的方法......由于现代 CPU 将其作为原语支持,因此开销要少得多。
挥发性本身是不正确的。尝试检索然后写回修改值的线程仍可能与执行相同操作的另一个线程发生冲突。
我赞同乔恩·斯基特的回答,并希望为每个想了解更多有关“易失性”和互锁的人添加以下链接:
原子性、波动性和不变性是不同的,第一部分 - (埃里克·利珀特的编码神话般的冒险)
Sayonara Volatile -(2012 年 Joe Duffy 博客的 Wayback Machine 快照)
我做了一些测试来看看这个理论实际上是如何运作的: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html. 。我的测试更侧重于 CompareExchnage,但 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
, Interlocked
, , 和 lock
:
- 参考类型。
- 指针类型(在不安全的上下文中)。请注意,虽然指针本身可以是易失性的,但它指向的对象却不能。在其他方面 也就是说,不能将 "指针 "声明为 "易失性"。
- 简单类型如
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
, , 和bool
. - 具有以下基本类型之一的枚举类型:
byte
,sbyte
,short
, 短写,int
, , 或者uint
. - 已知为引用类型的通用类型参数。
IntPtr
和UIntPtr
.
其他类型, , 包括 double
和 long
, 不能标记为 "易变"
因为无法保证对这些类型字段的读取和写入
是原子的。为保护多线程访问这些类型的
字段,使用 Interlocked
类成员或使用以下方式保护访问lock
陈述。