最糟糕的方法(实际上不起作用)
将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与增量结合使用。
Interlocked注意事项:
- 交错方法在任何数量的核心或CPU上都是并发安全的。
- 交错方法在执行它们的指令周围应用了一个完整的屏障,因此不会进行重新排序。
- 交错方法不需要甚至不支持对易失字段的访问,因为易失被放置在给定字段的半屏障周围,而交错使用完整的屏障。
注释:易失实际上有什么作用。
既然volatile
不能解决这些类型的多线程问题,那么它有什么用呢?一个很好的例子是你有两个线程,一个总是写入一个变量(比如queueLength
),一个总是从同一变量中读取。
如果queueLength
不是易失的,线程A可能会写入五次,但线程B可能会看到这些写入被延迟(甚至可能在错误的顺序中)。
一个解决方案是加锁,但你也可以在这种情况下使用易失。这将确保线程B始终能看到线程A写入的最新内容。然而需要注意的是,仅当你具有从未读取的写入者和从未写入的读取者以及正在写入的内容是原子值时,才能使用此逻辑。一旦进行单个读取修改写入操作,就需要使用交错操作或锁定。