易失性 vs. 互锁 vs. 锁定 [英] Volatile vs. Interlocked vs. lock

查看:25
本文介绍了易失性 vs. 互锁 vs. 锁定的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设一个类有一个被多个线程访问的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 上运行的多个线程可以并且将会缓存数据和重新排序指令.

如果它not 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 与增量结合起来.

互锁笔记:

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

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

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

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

解决方案是锁定,但在这种情况下您也可以使用 volatile.这将确保线程 B 将始终看到线程 A 编写的最新内容.但是请注意,此逻辑在您有从不阅读的作者和从不写作的读者以及如果您正在编写的内容是原子值时有效.一旦你做了一个单一的读-修改-写,你就需要去互锁操作或使用一个锁.

Let's say that a class has a public int counter field that is accessed by multiple threads. This int is only incremented or decremented.

To increment this field, which approach should be used, and why?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • Change the access modifier of counter to public volatile.

Now that I've discovered volatile, I've been removing many lock statements and the use of Interlocked. But is there a reason not to do this?

解决方案

Worst (won't actually work)

Change the access modifier of counter to public volatile

As other people have mentioned, this on its own isn't actually safe at all. The point of volatile is that multiple threads running on multiple CPUs can and will cache data and re-order instructions.

If it is not volatile, and CPU A increments a value, then CPU B may not actually see that incremented value until some time later, which may cause problems.

If it is volatile, this just ensures the two CPUs see the same data at the same time. It doesn't stop them at all from interleaving their reads and write operations which is the problem you are trying to avoid.

Second Best:

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

This is safe to do (provided you remember to lock everywhere else that you access this.counter). It prevents any other threads from executing any other code which is guarded by locker. Using locks also, prevents the multi-CPU reordering problems as above, which is great.

The problem is, locking is slow, and if you re-use the locker in some other place which is not really related then you can end up blocking your other threads for no reason.

Best

Interlocked.Increment(ref this.counter);

This is safe, as it effectively does the read, increment, and write in 'one hit' which can't be interrupted. Because of this, it won't affect any other code, and you don't need to remember to lock elsewhere either. It's also very fast (as MSDN says, on modern CPUs, this is often literally a single CPU instruction).

I'm not entirely sure however if it gets around other CPUs reordering things, or if you also need to combine volatile with the increment.

InterlockedNotes:

  1. INTERLOCKED METHODS ARE CONCURRENTLY SAFE ON ANY NUMBER OF COREs OR CPUs.
  2. Interlocked methods apply a full fence around instructions they execute, so reordering does not happen.
  3. Interlocked methods do not need or even do not support access to a volatile field, as volatile is placed a half fence around operations on given field and interlocked is using the full fence.

Footnote: What volatile is actually good for.

As volatile doesn't prevent these kinds of multithreading issues, what's it for? A good example is saying you have two threads, one which always writes to a variable (say queueLength), and one which always reads from that same variable.

If queueLength is not volatile, thread A may write five times, but thread B may see those writes as being delayed (or even potentially in the wrong order).

A solution would be to lock, but you could also use volatile in this situation. This would ensure that thread B will always see the most up-to-date thing that thread A has written. Note however that this logic only works if you have writers who never read, and readers who never write, and if the thing you're writing is an atomic value. As soon as you do a single read-modify-write, you need to go to Interlocked operations or use a Lock.

这篇关于易失性 vs. 互锁 vs. 锁定的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆