易失与互锁与锁定
假设一个类有一个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++
;
这是安全的(只要你记得在其他地方lock
你访问this.counter
)。 它阻止任何其他线程执行任何其他由locker
保护的代码。 使用锁也可以防止多CPU重新排序问题,这很好。
问题是,锁定速度很慢,如果你在其他地方重新使用locker
,那么你可能会无缘无故地阻塞其他线程。
最好
Interlocked.Increment(ref this.counter);
这是安全的,因为它有效地进行了阅读,增加和写入“一击”,不能被打断。 正因为如此,它不会影响任何其他代码,也不需要记住在其他地方锁定。 它也非常快(正如MSDN所说,在现代CPU上,这通常是单个CPU指令)。
但我不完全确定,如果它绕过其他CPU重新排序,或者你还需要将volatile与增量结合起来。
InterlockedNotes:
脚注:什么波动是真正有益的。
由于volatile
不能防止这种多线程问题,它有什么作用? 一个很好的例子就是说你有两个线程,一个总是写入一个变量(比如queueLength
),另一个总是读取同一个变量。
如果queueLength
不是易失性的,则线程A可能会写入五次,但线程B可能会将这些写入视为延迟(甚至可能以错误的顺序)。
解决办法是锁定,但在这种情况下你也可以使用volatile。 这将确保线程B将始终看到线程A写入的最新内容。 但请注意,如果您有从未阅读的作家,以及从不写作的读者,以及您正在撰写的内容是原子值,则此逻辑才有效。 只要您执行单个读取 - 修改 - 写入,您需要进入联锁操作或使用锁定。
编辑:正如评论中指出的那样,现在我很乐意使用Interlocked
来处理单个变量的情况,它显然是可以的。 当它变得更复杂时,我仍然会恢复锁定...
当您需要增加时,使用volatile
将无济于事,因为读取和写入是单独的指令。 读完之后但在写回之前,另一个线程可能会更改此值。
就个人而言,我几乎总是锁定 - 以一种明显正确的方式比波动性或互锁增长更容易。 就我而言,无锁多线程适用于真正的线程专家,我不是其中一员。 如果Joe Duffy和他的团队构建了很好的库,这些库能够并行处理事务,而不会像构建的东西那样锁定,那就太棒了,我会用心跳 - 但是当我自己执行线程时,我会尝试把事情简单化。
“ volatile
”不会取代Interlocked.Increment
! 它只是确保变量不被缓存,但直接使用。
增加一个变量实际上需要三个操作:
Interlocked.Increment
执行所有三个部分作为一个单一的原子操作。
上一篇: Volatile vs. Interlocked vs. lock
下一篇: The code example which can prove "volatile" declare should be used