一尘不染

易失性,连锁性与锁定

c#

假设一个类具有一个public int counter可由多个线程访问的字段。这int仅递增或递减。

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

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

现在我已经发现volatile,我已经删除了许多lock语句和对的使用Interlocked。但是,有理由不这样做吗?


阅读 315

收藏
2020-05-19

共1个答案

一尘不染

最差(实际上不会工作)

将的访问修饰符更改counterpublic 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. 互锁方法 不需要甚至不支持访问volatile字段 ,因为volatile在给定字段上的操作周围放置了半围墙,而联锁使用的是全围墙。

脚注:挥发物实际上是有益的。

由于volatile不能防止此类多线程问题,它的用途是什么?一个很好的例子是说您有两个线程,一个线程总是写一个变量(例如queueLength),而一个线程总是从同一个变量读取。

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

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

2020-05-19