753

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

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

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

现在我发现了volatile,我一直在删除许多lock语句和Interlocked. 但是有理由不这样做吗?

4

10 回答 10

955

最差(实际上不起作用)

将访问修饰符更改为countertopublic 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 重新排序,或者您是否还需要将 volatile 与增量相结合。

联锁注意事项:

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

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

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

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

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

于 2008-09-30T20:13:08.237 回答
157

编辑:正如评论中所指出的,这些天我很高兴将其Interlocked用于显然可以的单个变量的情况。当它变得更复杂时,我仍然会恢复锁定......

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

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

于 2008-09-30T19:29:35.003 回答
47

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

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

  1. 增量

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

于 2008-09-30T19:32:52.327 回答
44

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

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 .

于 2011-06-23T15:08:37.957 回答
18

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

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

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

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

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

于 2008-09-30T19:27:22.403 回答
13

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

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

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

于 2008-09-30T19:32:09.473 回答
10

我做了一些测试以了解该理论的实际工作原理: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
于 2009-05-24T23:12:18.533 回答
4

我想补充其他答案中提到的volatile,Interlocked和之间的区别lock

volatile 关键字可以应用于以下类型的字段

  • 引用类型。
  • 指针类型(在不安全的上下文中)。请注意,尽管指针本身可以是易失的,但它指向的对象不能。换句话说,您不能将“指针”声明为“易失性”。
  • 简单类型,例如sbyte, byte, short, ushort, int, uint, char, float, 和bool.
  • 具有以下基本类型之一的枚举类型:bytesbyteshort、 ushort int、 或uint
  • 已知为引用类型的泛型类型参数。
  • IntPtrUIntPtr

其他类型,包括doubleand long,不能标记为“易失性”,因为不能保证对这些类型的字段的读写是原子的。要保护对这些类型字段的多线程访问,请使用Interlocked类成员或使用 lock语句保护访问。

于 2018-12-02T15:51:32.287 回答
0

我只是在这里指出 Orion Edwards 回答中关于 volatile 的错误。

他说:

“如果它是易失性的,这只是确保两个 CPU 同时看到相同的数据。”

这是错的。在微软关于volatile的文档中,提到:

“在多处理器系统上,易失性读操作不能保证获得任何处理器写入该内存位置的最新值。同样,易失性写操作不能保证写入的值立即对其他处理器可见。”

于 2022-01-21T07:39:41.097 回答