易失性与互锁性与锁定性

Posted

技术标签:

【中文标题】易失性与互锁性与锁定性【英文标题】:Volatile vs. Interlocked vs. lock 【发布时间】:2010-09-14 08:23:38 【问题描述】:

假设一个类有一个由多个线程访问的public int counter 字段。这个int 只会递增或递减。

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

lock(this.locker) this.counter++;, Interlocked.Increment(ref this.counter);, 将counter的访问修饰符更改为public volatile

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

【问题讨论】:

阅读Threading in C# 参考。它涵盖了您问题的来龙去脉。三者中的每一个都有不同的目的和副作用。 simple-talk.com/blogs/2012/01/24/… 你可以看到 volitable 在数组中的使用,我不完全理解,但这是对它的另一个参考。 这就像在说“我发现自动喷水灭火系统从未启动过,所以我打算将其移除并用烟雾报警器替换它”。不这样做的原因是因为它非常危险并且几乎没有任何好处。如果您有时间花在更改代码上,那么想办法减少它的多线程!不要想办法让多线程代码更危险、更容易被破坏! 我家有洒水器烟雾报警器。当在一个线程上增加一个计数器并在另一个线程上读取它时,您似乎需要一个锁(或一个互锁) volatile关键字。真相? @yoyo 不,你不需要两者。 【参考方案1】:

最差(实际上不起作用)

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

正如其他人所提到的,这本身并不安全。 volatile 的要点是,在多个 CPU 上运行的多个线程可以并且将会缓存数据并重新排序指令。

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

如果是volatile,这只是确保两个CPU同时看到相同的数据。它根本不会阻止他们交错读取和写入操作,这是您要避免的问题。

第二佳:

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

这样做是安全的(只要您记得在您访问this.counter 的其他任何地方都使用lock)。它防止任何其他线程执行由locker 保护的任何其他代码。 使用锁也可以防止上面的多 CPU 重新排序问题,这很棒。

问题是,锁定很慢,如果您在其他一些不相关的地方重新使用locker,那么您最终可能会无缘无故地阻塞您的其他线程。

最佳

Interlocked.Increment(ref this.counter);

这是安全的,因为它有效地执行了不可中断的“一击”读取、递增和写入操作。正因为如此,它不会影响任何其他代码,你也不需要记住在其他地方锁定。它也非常快(正如 MSDN 所说,在现代 CPU 上,这通常实际上是一条 CPU 指令)。

我不完全确定它是否可以绕过其他 CPU 重新排序,或者您是否还需要将 volatile 与增量相结合。

联锁笔记:

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

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

volatile 无法防止此类多线程问题,它有什么用?一个很好的例子是说你有两个线程,一个总是写入一个变量(比如queueLength),一个总是从同一个变量中读取。

如果queueLength 不是易失性的,线程 A 可能会写入五次,但线程 B 可能会将这些写入视为延迟(甚至可能是错误的顺序)。

一种解决方案是锁定,但在这种情况下您也可以使用 volatile。这将确保线程 B 将始终看到线程 A 编写的最新内容。但是请注意,此逻辑适用于您有从不阅读的作者和从不写作的读者,并且如果您正在编写的内容是原子值。一旦您执行了一次读取-修改-写入,您需要转到互锁操作或使用锁。

【讨论】:

“我不完全确定……你是否还需要将 volatile 与增量结合起来。”它们不能结合 AFAIK,因为我们不能通过 ref 传递 volatile。顺便说一句,答案很好。 非常感谢!您在“什么 volatile 实际上有好处”的脚注是我一直在寻找的内容,并确认了我想如何使用 volatile。 换句话说,如果一个 var 被声明为 volatile,编译器将假定每次代码遇到 var 的值都不会保持不变(即 volatile)。因此,在诸如 while (m_Var) 之类的循环中,并且 m_Var 在另一个线程中设置为 false,编译器不会简单地检查先前加载了 m_Var 值的寄存器中的内容,而是从 m_Var 中读取值再次。但是,这并不意味着不声明 volatile 会导致循环无限进行 - 指定 volatile 仅保证在另一个线程中将 m_Var 设置为 false 时不会。 @Zach Saw:在 C++ 的内存模型下,volatile 就是您所描述的(基本上对设备映射内存有用,而不是其他很多)。在 CLR 的内存模型下(这个问题被标记为 C#)是 volatile 将在对该存储位置的读取和写入周围插入内存屏障。内存屏障(以及某些汇编指令的特殊锁定变体)是你告诉 处理器 不要重新排序的东西,它们相当重要...... @ZachSaw:C# 中的 volatile 字段阻止 C# 编译器和 jit 编译器进行某些会缓存值的优化。它还对可以在多个线程上观察到的读取和写入顺序做出某些保证。作为一个实现细节,它可以通过在读写时引入内存屏障来实现。规范中描述了保证的精确语义;请注意,规范保证all易失性写入和读取的一致顺序将被all观察到线程。【参考方案2】:

编辑:正如 cmets 中所述,这些天我很高兴将Interlocked 用于单个变量的情况,显然嗯>好的。当它变得更复杂时,我仍然会恢复锁定......

当您需要递增时,使用 volatile 将无济于事 - 因为读取和写入是单独的指令。另一个线程可能会在您读取之后但在您回写之前更改该值。

就我个人而言,我几乎总是只锁定 - 以一种显然正确的方式比波动性或 Interlocked.Increment 更容易。就我而言,无锁多线程适用于真正的线程专家,我不是其中之一。如果 Joe Duffy 和他的团队构建了很好的库,可以并行化事物而没有像我构建的东西那样多的锁定,那太棒了,我会立即使用它 - 但是当我自己做线程时,我会尝试保持简单。

【讨论】:

+1 确保我从现在开始忘记无锁编码。 无锁代码绝对不是真正无锁的,因为它们在某个阶段锁定 - 无论是在 (FSB) 总线还是 interCPU 级别,您仍然需要支付罚金。但是,只要您不使发生锁定的带宽饱和,在这些较低级别进行锁定通常会更快。 Interlocked 没有任何问题,它正是您想要的,而且比全锁() 更快 @Jaap:是的,这些天我使用 interlocked 作为真正的单一计数器。我只是不想开始尝试解决对变量的多个无锁更新之间的交互。 @ZachSaw:您的第二条评论说联锁操作在某个阶段“锁定”;术语“锁定”通常意味着一个任务可以在无限长的时间内保持对资源的独占控制;无锁编程的主要优点是它避免了由于拥有的任务被搁置而导致资源变得不可用的危险。互锁类使用的总线同步不仅仅是“通常更快”——在大多数系统上,它有一个有界的最坏情况时间,而锁则没有。【参考方案3】:

volatile”不能代替Interlocked.Increment!它只是确保变量没有被缓存,而是直接使用。

增加一个变量实际上需要三个操作:

    阅读 增量 写

Interlocked.Increment 将所有三个部分作为单个原子操作执行。

【讨论】:

换一种说法,联锁更改是完全防护的,因此是原子的。易失性成员只是部分被隔离,因此不能保证是线程安全的。 实际上,volatile 确实 not 确保变量没有被缓存。它只是限制了如何缓存它。例如,它仍然可以缓存在 CPU 的二级缓存中,因为它们在硬件中是一致的。它仍然可以完善。写入仍然可以发布到缓存,依此类推。 (我认为这就是 Zach 的意思。)【参考方案4】:

您正在寻找锁定或互锁增量。

Volatile 绝对不是您想要的 - 它只是告诉编译器将变量视为始终在变化,即使当前代码路径允许编译器以其他方式优化从内存读取。

例如

while (m_Var)
 

如果 m_Var 在另一个线程中设置为 false 但它没有声明为 volatile,编译器可以通过检查 CPU 寄存器(例如 EAX因为那是 m_Var 从一开始就被提取的内容)而不是向 m_Var 的内存位置发出另一次读取(这可能被缓存 - 我们不知道也不关心,这就是 x86 的缓存一致性点/ x64)。其他人之前提到指令重新排序的所有帖子都只是表明他们不了解 x86/x64 架构。 Volatile 确实发出读/写障碍,正如之前的帖子所说的“它防止重新排序”所暗示的那样。事实上,再次感谢 MESI 协议,我们可以保证我们读取的结果在 CPU 之间始终是相同的,无论实际结果是否已退休到物理内存或只是驻留在本地 CPU 的缓存中。我不会过多介绍这个细节,但请放心,如果出现问题,英特尔/AMD 可能会召回处理器!这也意味着我们不必关心乱序执行等。结果总是保证按顺序退出 - 否则我们会被塞满!

使用 Interlocked Increment,处理器需要退出,从给定地址获取值,然后递增并将其写回 - 同时拥有整个高速缓存行的独占所有权(锁定 xadd)以确保没有其他处理器可以修改它的值。

使用 volatile,您最终仍将只得到 1 条指令(假设 JIT 应该是高效的) - inc dword ptr [m_Var]。但是,处理器 (cpuA) 在执行与互锁版本所做的所有操作时,并不要求对高速缓存行进行独占所有权。可以想象,这意味着其他处理器可以在 cpuA 读取更新值后将其写回 m_Var。因此,现在不是将值增加两次,而是最终只增加了一次。

希望这能解决问题。

有关详细信息,请参阅“了解低锁技术在多线程应用程序中的影响”-http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

附言是什么促使了这个很晚的回复?在他们的解释中,所有回复都非常不正确(尤其是标记为答案的回复),我只需要为其他阅读本文的人清除它。 耸耸肩

p.p.s.我假设目标是 x86/x64 而不是 IA64(它有不同的内存模型)。请注意,Microsoft 的 ECMA 规范被搞砸了,因为它指定了最弱的内存模型而不是最强的内存模型(最好针对最强的内存模型进行指定,以便它在平台之间保持一致 - 否则代码将在 x86/ 上运行 24-7 x64 可能根本无法在 IA64 上运行,尽管 Intel 已经为 IA64 实现了类似的强内存模型)——微软自己承认了这一点——http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx。

【讨论】:

有趣。你可以参考这个吗?我很乐意对此投赞成票,但是在获得与我所阅读的资源一致的高度投票答案 3 年后,使用一些激进的语言发布将需要更多切实的证据。 为什么有人想要阻止 CPU 缓存,我无法理解。在这种情况下,专用于执行缓存一致性的整个空间(在大小和成本上绝对不可忽略)完全浪费了......除非您不需要缓存一致性,例如显卡、PCI 设备等,否则您不会设置用于直写的高速缓存行。 是的,你所说的一切如果不是 100%,至少 99% 都在标记上。当您在工作中急于开发时,该站点(大部分)非常有用,但不幸的是,与(游戏)投票相对应的答案的准确性不存在。所以基本上在***中你可以感受到读者的流行理解是什么,而不是真正的理解。有时,最重要的答案只是纯粹的胡言乱语——善意的神话。不幸的是,这就是在解决问题时遇到阅读的人们的原因。这是可以理解的,没有人可以知道一切。 这个答案的问题,以及在这个问题上到处都是你的 cmets,是它是 x86 独有的,而问题不是。了解底层硬件内存模型有时很有用,但不能代替 CLR 内存模型的知识。例如,仅仅因为内存屏障在 x86 上是隐式的,并不意味着 CLR 内存模型不需要 volatile 的内存屏障(比 C++ volatile 更多)。 .NET 代码在六种架构上运行,而 C++ 远不止这些。 @BenVoigt 我可以继续回答 .NET 运行的所有架构,但这需要几页,而且绝对不适合 SO。基于最广泛使用的 .NET 底层硬件 mem 模型来教育人们比随意的教育要好得多。通过我的 cmets '无处不在',我正在纠正人们在假设刷新/使缓存无效等方面所犯的错误。他们对底层硬件进行了假设,但没有指定哪些硬件。【参考方案5】:

联锁功能不会锁定。它们是原子的,这意味着它们可以在增量期间完成而无需上下文切换的可能性。所以没有死锁或等待的机会。

我会说你应该总是更喜欢它而不是锁和增量。

如果您需要在一个线程中写入以在另一个线程中读取,并且如果您希望优化器不对变量重新排序操作(因为优化器不知道的另一个线程中发生的事情),Volatile 很有用。这是您如何递增的正交选择。

如果您想了解更多关于无锁代码以及编写它的正确方法的信息,这是一篇非常好的文章

http://www.ddj.com/hpc-high-performance-computing/210604448

【讨论】:

【参考方案6】:

lock(...) 有效,但可能会阻塞线程,并且如果其他代码以不兼容的方式使用相同的锁,可能会导致死锁。

Interlocked.* 是正确的方法...现代 CPU 将其作为原语支持,因此开销要小得多。

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

【讨论】:

【参考方案7】:

我做了一些测试来看看这个理论是如何运作的:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。我的测试更侧重于 CompareExchnage,但 Increment 的结果是相似的。在多 cpu 环境中不需要更快地互锁。这是在 2 岁的 16 CPU 服务器上的增量测试结果。请记住,测试还涉及增加后的安全读取,这在现实世界中很常见。

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

【讨论】:

您测试的代码示例实在是太简单了——这样测试真的没有多大意义!最好的办法是了解不同方法的实际作用,并根据您的使用场景使用适当的方法。 @Zach,这里的讨论是关于以线程安全方式增加计数器的场景。您想到了哪些其他使用场景,或者您将如何测试它?感谢您的评论顺便说一句。 重点是,这是人为的测试。在任何现实世界的场景中,您都不会经常敲击相同的位置。如果你是,那么你就会受到 FSB 的限制(如你的服务器框所示)。不管怎样,看看我在你博客上的回复。 再次回顾。如果真正的瓶颈在于 FSB,则监视器实现应该观察到相同的瓶颈。真正的区别在于 Interlocked 正在忙等待和重试,这成为高性能计数的真正问题。至少我希望我的评论能引起人们的注意,即互锁并不总是计数的正确选择。人们正在寻找替代方案的事实很好地解释了这一点。你需要一个长加法器gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…【参考方案8】:

我赞同 Jon Skeet 的回答,并想为所有想了解更多关于“易失性”和 Interlocked 的人添加以下链接:

Atomicity, volatility and immutability are different, part one - (Eric Lippert's Fabulous Adventures In Coding)

Atomicity, volatility and immutability are different, part two

Atomicity, volatility and immutability are different, part three

Sayonara Volatile - (Wayback Machine snapshot of Joe Duffy's Weblog as it appeared in 2012)

【讨论】:

【参考方案9】:

我想补充其他答案中提到的volatileInterlockedlock 之间的区别:

The volatile keyword can be applied to fields of these types:

引用类型。 指针类型(在不安全的上下文中)。请注意,尽管指针本身可以是易失的,但它指向的对象不能。其他 换句话说,您不能将“指针”声明为“易失性”。 sbytebyteshortushortintuintcharfloatbool等简单类型。 具有以下基本类型之一的枚举类型:bytesbyteshort、ushort、intuint。 已知为引用类型的泛型类型参数。 IntPtrUIntPtr

其他类型,包括doublelong,不能标记为“volatile” 因为无法保证对这些类型的字段的读写 是原子的。保护对这些类型的多线程访问 字段,使用 Interlocked 类成员或使用 lock 声明。

【讨论】:

以上是关于易失性与互锁性与锁定性的主要内容,如果未能解决你的问题,请参考以下文章

巨杉数据库SequoiaDB巨杉 Tech | 并发性与锁机制解析与实践

C# Max 计数器与互锁 [重复]

ABAP锁,数据库锁

Visual Studio 2013 C++ 本机代码中与互锁操作的线程同步挂起

与锁相比,原子/互锁变量有多快,无论是不是存在争用? [复制]

将易失性数组与非易失性数组进行比较