协慌网

登录 贡献 社区

易失性与连锁性与锁定

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

如果它不是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不能防止此类多线程问题,它的用途是什么?一个很好的例子是说您有两个线程,一个线程总是写一个变量(比如queueLength ),而另一个线程总是从同一个变量中读取。

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

一种解决方案是锁定,但在这种情况下也可以使用 volatile。这样可以确保线程 B 始终可以看到线程 A 编写的最新内容。但是请注意,只有当您有从未读过的作家和从未写过的读者,并且您要写的东西是原子值时,此逻辑才有效。一旦完成一次读 - 修改 - 写操作,就需要进入 “互锁” 操作或使用 “锁定”。

编辑:正如评论中指出的那样,这些天来,我很乐意在单个变量明显可用的情况下Interlocked 。当事情变得更加复杂时,我仍将恢复锁定状态。

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

就我个人而言,我几乎总是锁定 - 以明显正确的方式比波动或 Interlocked.Increment 更容易正确。就我而言,无锁多线程是针对真正的线程专家的,而我不是。如果 Joe Duffy 和他的团队构建了不错的库,这些库可以并行化事物而又没有我要构建的东西那么多,那真是太好了,我将在心跳中使用它 - 但是当我自己进行线程化时,我会尝试把事情简单化。

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

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

  1. 增量

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