问答中心分类: MULTITHREADING易失性与互锁性与锁定性
0
匿名用户 提问 18分钟 前

假设一个类有一个public int counter由多个线程访问的字段。这个int只是增加或减少。
要增加这个字段,应该使用哪种方法,为什么?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • 更改访问修饰符counterpublic volatile.

现在我发现了volatile, 我已经删除了很多lock声明和使用Interlocked.但是有理由不这样做吗?

spoulson 回复 18分钟 前

阅读C#中的线程参考。它涵盖了您问题的来龙去脉。三者中的每一个都有不同的目的和副作用。

spoulson 回复 18分钟 前

simple-talk.com/blogs/2012/01/24/…你可以看到 volitable 在数组中的使用,我不完全理解它,但它是对它的另一个参考。

spoulson 回复 18分钟 前

这就像在说“我发现自动喷水灭火系统从未启动过,所以我要拆除它并用烟雾报警器代替它”。不这样做的原因是因为它非常危险几乎没有任何好处.如果您有时间花时间更改代码,那么找到一种减少多线程的方法!不要想办法让多线程代码更危险、更容易被破坏!

spoulson 回复 18分钟 前

我家有两个洒水器烟雾报警器。当在一个线程上增加一个计数器并在另一个线程上读取它时,您似乎需要一个锁(或一个互锁)volatile 关键字。真相?

spoulson 回复 18分钟 前

@yoyo不,你不需要两者。

spoulson 回复 18分钟 前

harshmaurya.in/volatile-vs-lock-vs-interlocked-in-c-net我试图解释这三者之间的区别。

9 Answers
0
Jon Skeet 回答 18分钟 前

编辑:如评论中所述,这些天我很高兴使用Interlocked对于一个单变量它在哪里明显地好的。当它变得更复杂时,我仍然会恢复锁定……
使用volatile当您需要增加时将无济于事 – 因为读取和写入是单独的指令。另一个线程可以在您阅读之后但在您回写之前更改该值。
就我个人而言,我几乎总是只是锁定 – 以一种更容易正确的方式明显地比波动性或 Interlocked.Increment 正确。就我而言,无锁多线程适用于真正的线程专家,我不是其中之一。如果 Joe Duffy 和他的团队构建了不错的库,可以并行化事物而没有像我构建的东西那样多的锁定,那就太棒了,我会立即使用它 – 但是当我自己做线程时,我会尝试把事情简单化。

Xaqron 回复 18分钟 前

+1 确保我从现在开始忘记无锁编码。

Zach Saw 回复 18分钟 前

无锁代码绝对不是真正的无锁代码,因为它们在某个阶段锁定 – 无论是在 (FSB) 总线还是 interCPU 级别,您仍然需要支付罚款。但是,只要您不使发生锁定的带宽饱和,在这些较低级别进行锁定通常会更快。

Jaap 回复 18分钟 前

Interlocked 没有任何问题,它正是您所寻找的,并且比完整的 lock() 更快

Jon Skeet 回复 18分钟 前

@Jaap:是的,这些天我使用联锁为真正的单一计数器。我只是不想开始搞乱试图解决之间的交互对变量的无锁更新。

supercat 回复 18分钟 前

@ZachSaw:“无锁”并不意味着无论竞争如何,一段代码都将始终以相同的速度运行。相反,这意味着由于另一个线程被无限期地搁置,一个线程可以等待多长时间是有限制的(在基于锁的代码中,如果一个线程在持有锁时被搁置,则需要锁的线程可能被无限期封锁)。请注意,仅仅无锁并不能保证代码不必无限期地等待竞争激烈的资源,但是Interlocked函数也提供后一种保证。

Zach Saw 回复 18分钟 前

@supercat:嗯?你在回复谁或什么??

supercat 回复 18分钟 前

@ZachSaw:我正在回复您的第二条评论;尽管它是不久前写的,但我只是想澄清一下,在现代 CPU 上互锁操作是无锁的,现代 CPU 包括防止总线锁被无限期持有的硬件。

Zach Saw 回复 18分钟 前

@supercat:我说哪条评论互锁操作会导致总线锁被无限期持有?

supercat 回复 18分钟 前

@ZachSaw:您的第二条评论说联锁操作在某个阶段“锁定”;术语“锁定”通常意味着一个任务可以在无限长的时间内保持对资源的独占控制;无锁编程的主要优点是它避免了由于拥有的任务被搁置而导致资源变得不可用的危险。互锁类使用的总线同步不仅仅是“通常更快”——在大多数系统上,它具有有限的最坏情况时间,而锁则没有。

Zach Saw 回复 18分钟 前

@supercat:如果您这样解释“锁定”,那么包含“锁定”一词的“互锁”将意味着同一件事——显然不是!

Teoman shipahi 回复 18分钟 前

如果你说“无锁多线程是为真正的线程专家准备的,我不是其中之一。”我永远不会尝试编写无锁多线程代码。 🙂

0
Michael Damatov 回答 18分钟 前

volatile” 不代替Interlocked.Increment!它只是确保变量没有被缓存,而是直接使用。
增加一个变量实际上需要三个操作:

  1. 增量

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

JoeGeeky 回复 18分钟 前

换句话说,互锁的变化是完全封闭的,因此是原子的。易失性成员仅部分受到保护,因此不能保证是线程安全的。

David Schwartz 回复 18分钟 前

实际上,volatile不是确保变量没有被缓存。它只是限制了如何缓存它。例如,它仍然可以缓存在 CPU 的二级缓存中,因为它们在硬件中是一致的。它仍然可以完善。写入仍然可以发布到缓存,依此类推。 (我认为这就是 Zach 的意思。)

0
Zach Saw 回答 18分钟 前

您正在寻找锁定或互锁增量。
Volatile 绝对不是您所追求的 – 它只是告诉编译器将变量视为始终在变化,即使当前代码路径允许编译器以其他方式优化从内存读取。
例如

while (m_Var)
{ }

如果在另一个线程中将 m_Var 设置为 false,但它没有声明为 volatile,则编译器可以通过检查 CPU 寄存器(例如 EAX,因为那是m_Var 从一开始就被提取到了什么)而不是向 m_Var 的内存位置发出另一次读取(这可能被缓存 – 我们不知道也不关心,这就是 x86/x64 的缓存一致性点)。其他人之前提到指令重新排序的所有帖子都只是表明他们不了解 x86/x64 架构。挥发性的不是正如前面的帖子所暗示的那样,发出读/写障碍,说“它可以防止重新排序”。事实上,再次感谢 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
ps 是什么导致了这个很晚的回复?所有回复在他们的解释中都非常不正确(尤其是标记为答案的回复),我只需要为其他阅读本文的人清理它。耸耸肩
pps 我假设目标是 x86/x64 而不是 IA64(它具有不同的内存模型)。请注意,Microsoft 的 ECMA 规范被搞砸了,因为它指定了最弱的内存模型而不是最强的内存模型(最好针对最强的内存模型进行指定,以便跨平台保持一致 – 否则代码将在 x86/ 上运行 24-7 x64 可能根本无法在 IA64 上运行,尽管英特尔已经为 IA64 实现了类似的强大内存模型)——微软自己承认了这一点——http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.

Steven Evers 回复 18分钟 前

有趣的。你可以参考这个吗?我很乐意对此投赞成票,但是在获得与我所阅读的资源一致的高度投票答案三年后,用一些激进的语言发布将需要更多切实的证据。

Zach Saw 回复 18分钟 前

如果你能指出你想引用哪个部分,我很乐意从某个地方挖掘一些东西(我非常怀疑我已经泄露了任何 x86/x64 供应商的商业机密,所以这些应该很容易从 wiki、Intel PRM(程序员参考手册)、MSFT 博客、MSDN 或类似的东西)…

Zach Saw 回复 18分钟 前

另外,我不认为这与其他人的回答不一致,只是在他们的解释中——表明 volatile 阻止 CPU 缓存变量的结果。这太荒谬了。这与你读过的任何东西有什么一致性?如果您可以在 x86/x64 中找到任何允许您将 32 位/64 位宽的内存位置设置为直写缓存或 Windows 允许客户端动态更改特定内存位置以进行直写的内容,以及然后在 GC 压缩堆时相应地调整它,从而绕过 CPU 缓存……

Zach Saw 回复 18分钟 前

为什么有人想要阻止 CPU 缓存,这超出了我的理解。在这种情况下,专用于执行缓存一致性的整个空间(在大小和成本上绝对不可忽略)完全浪费了……除非您不需要缓存一致性,例如显卡、PCI 设备等,否则您不会设置要写入的高速缓存行。

user1416420 回复 18分钟 前

是的,你所说的一切如果不是 100%,至少是 99%。当您在工作中急于开发时,该站点(大部分)非常有用,但不幸的是,与(游戏)投票相对应的答案的准确性不存在。所以基本上在stackoverflow中你可以感受到读者的流行理解是什么,而不是真正的理解。有时,最重要的答案只是纯粹的胡言乱语——善意的神话。不幸的是,这就是在解决问题时遇到阅读的人们的原因。不过可以理解,没有人能知道一切。

Ben Voigt 回复 18分钟 前

这个答案的问题,以及你对这个问题的评论,是它是 x86 独有的,而问题不是。了解底层硬件内存模型有时很有用,但不能代替 CLR 内存模型的知识。例如,仅仅因为内存屏障在 x86 上是隐式的,并不意味着 CLR 内存模型不需要内存屏障volatile(超过 C++volatile)。 .NET 代码在六种架构上运行,而 C++ 远不止这些。

Zach Saw 回复 18分钟 前

@BenVoigt我可以继续回答所有.NET运行的架构,但这需要几页,而且绝对不适合SO。基于最广泛使用的 .NET 底层硬件 mem 模型来教育人们比随意的教育要好得多。通过我的“无处不在”的评论,我纠正了人们在假设刷新/使缓存无效等方面所犯的错误。他们对底层硬件进行了假设,但没有指定哪些硬件。

Zach Saw 回复 18分钟 前

@BenVoigt 另请注意,微软从一开始就填充了他们的 ECMA 内存模型规范,因此对这样的问题没有全面的答案。这个问题也没有提到 CLR 内存模型。 ECMA 是标准,而不是 CLR。 ECMA 不强制要求内存屏障。所以,当你发表评论时,也许你只是在迂腐?

Ben Voigt 回复 18分钟 前

@Zach:即使是 ECMA 模型也为 volatile 访问提供了获取和/或释放语义(不是完全障碍,但这些也是障碍/栅栏的类型),不是吗?

Zach Saw 回复 18分钟 前

@BenVoigt:从内存中,它指定了弱内存模型-因此不能保证易失性访问。另一方面,CLR 在 IA64 上实现强内存模型。

Ben Voigt 回复 18分钟 前

@Zach:根据你链接到的微软的解释,没那么弱。它在 x86 上没有做任何额外的事情,因为 x86 本身提供了更强大的保证,但不要误以为这是volatile不提供任何保证。

Zach Saw 回复 18分钟 前

@BenVoigt:就像我在回答中所说的那样,我不会对任何其他架构做出任何假设(其他架构是——这促使我发布这个答案)。但是,如果没有参考(来自 ECMA std 上的官方 Microsoft 来源),这对我们双方来说都只是猜测。

Eric Ouellet 回复 18分钟 前

@ZachSaw,这里唯一的好答案,也是我读到的第一个答案,它真正清楚地解释了“易失性”的原因和含义。非常感谢你。

Yarl 回复 18分钟 前

@ZachSaw • 8.10 执行顺序对于易失性读取和写入,副作用的顺序得以保留。• 15.5.4 易失性字段 ◾对易失性字段的读取称为易失性读取。易失性读取具有“获取语义”;也就是说,它保证在指令序列中对内存的任何引用之前发生。对易失性字段的写入称为易失性写入。易失性写入具有“释放语义”;也就是说,它保证发生在指令序列中写指令之前的任何内存引用之后。ECMA-334。

0
Lou Franco 回答 18分钟 前

联锁功能不锁定。它们是原子的,这意味着它们可以在增量期间完成而无需上下文切换。所以没有死锁或等待的机会。
我会说你应该总是更喜欢它而不是锁和增量。
如果您需要在一个线程中写入以在另一个线程中读取,并且如果您希望优化器不对变量的操作重新排序(因为优化器不知道的另一个线程中正在发生事情),那么 Volatile 很有用。这是您如何增加的正交选择。
如果您想了解更多关于无锁代码以及编写它的正确方法的信息,这是一篇非常好的文章
http://www.ddj.com/hpc-high-performance-computing/210604448

0
Rob Walker 回答 18分钟 前

lock(…) 有效,但可能会阻塞线程,并且如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。
Interlocked.* 是正确的方法……因为现代 CPU 将其作为原语支持,所以开销要少得多。
volatile 本身是不正确的。尝试检索然后写回修改值的线程仍可能与执行相同操作的另一个线程发生冲突。