假设一个类具有一个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 与增量结合起来。
连锁注意事项:
由于volatile
不能防止此类多线程问题,它的用途是什么?一个很好的例子是说您有两个线程,一个线程总是写一个变量(比如queueLength
),而另一个线程总是从同一个变量中读取。
如果queueLength
不是易失性的,线程 A 可能会写入五次,但线程 B 可能会认为这些写入被延迟(甚至可能以错误的顺序)。
一种解决方案是锁定,但在这种情况下也可以使用 volatile。这样可以确保线程 B 始终可以看到线程 A 编写的最新内容。但是请注意,只有当您有从未读过的作家和从未写过的读者,并且您要写的东西是原子值时,此逻辑才有效。一旦完成一次读 - 修改 - 写操作,就需要进入 “互锁” 操作或使用 “锁定”。
编辑:正如评论中指出的那样,这些天来,我很乐意在单个变量明显可用的情况下Interlocked
。当事情变得更加复杂时,我仍将恢复锁定状态。
当您需要递增时,使用volatile
将无济于事 - 因为读和写是单独的指令。读完之后但写回之前,另一个线程可能会更改该值。
就我个人而言,我几乎总是锁定 - 以明显正确的方式比波动或 Interlocked.Increment 更容易正确。就我而言,无锁多线程是针对真正的线程专家的,而我不是。如果 Joe Duffy 和他的团队构建了不错的库,这些库可以并行化事物而又没有我要构建的东西那么多,那真是太好了,我将在心跳中使用它 - 但是当我自己进行线程化时,我会尝试把事情简单化。
“ volatile
” 不能代替Interlocked.Increment
!它只是确保该变量不被缓存,而是直接使用。
递增变量实际上需要三个操作:
Interlocked.Increment
作为一个原子操作执行所有三个部分。