小编典典

易失性与互锁性与锁定性

all

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

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

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

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


阅读 129

收藏
2022-03-02

共1个答案

小编典典

最差(实际上不起作用)

将访问修饰符更改为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 编写的最新内容。但是请注意,此逻辑
适用于您有从不阅读的作者和从不写作的读者, 并且 您正在编写的内容是原子值的情况。一旦您执行了一次读取-修改-写入,您就需要进行互锁操作或使用锁。

2022-03-02