我们需要在multithreading代码中读取.NET Int32时锁定它吗?

我正在阅读以下文章: http : //msdn.microsoft.com/en-us/magazine/cc817398.aspx “解决你的multithreading代码中的11个可能的问题”作者:Joe Duffy

它提出了一个问题:“我们需要在multithreading代码中读取它时锁定.NET Int32吗?”

据我所知,如果它是32位SO中的Int64,它可能会撕裂,正如文章中所解释的那样。 但对于Int32,我想到了以下情况:

class Test { private int example = 0; private Object thisLock = new Object(); public void Add(int another) { lock(thisLock) { example += another; } } public int Read() { return example; } } 

我没有看到在Read方法中包含锁的原因。 你呢?

更新基于答案(由Jon Skeet和ctacke提供)我理解上面的代码仍然容易受到多处理器缓存的影响(每个处理器都有自己的缓存,与其他处理器不同步)。 所有这三个修改都解决了这个问题:

  1. 添加“int example”的“volatile”属性
  2. 插入Thread.MemoryBarrier(); 在实际读取“int example”之前
  3. 在“lock(thisLock)”中读取“int example”

我也认为“易变”是最优雅的解决方案。

锁定完成两件事:

  • 它充当互斥锁,因此您可以确保一次只有一个线程修改一组值。
  • 它提供了内存屏障(获取/释放语义),可确保一个线程的内存写入在另一个线程中可见。

大多数人都理解第一点,但不是第二点。 假设您使用了来自两个不同线程的问题中的代码,其中一个线程重复调用Add ,另一个线程调用Read 。 单独的primefaces性将确保您最终只读取8的倍数 – 如果有两个线程调用Add您的锁定将确保您没有“丢失”任何添加。 但是,即使在多次调用Add之后,您的Read线程很可能只读取0。 没有任何内存障碍,JIT可以将值缓存在寄存器中,并假设它在读取之间没有变化。 内存障碍的关键是确保某些内容真正写入主内存,或者确实从主内存中读取内容。

内存模型可能变得非常毛茸茸,但是如果你按照每次想要访问共享数据(读取写入)时取出锁的简单规则,你就可以了。 有关更多详细信息,请参阅我的线程教程的波动率/primefaces性部分。

这一切都取决于具体情况。 处理整数类型或引用时,您可能希望使用System.Threading.Interlocked类的成员。

典型用法如:

 if( x == null ) x = new X(); 

可以通过调用Interlocked.CompareExchange()来替换:

 Interlocked.CompareExchange( ref x, new X(), null); 

Interlocked.CompareExchange()保证比较和交换作为primefaces操作发生。

Interlocked类的其他成员,如Add()Decrement()Exchange()Increment()Read()都以primefaces方式执行各自的操作。 阅读MSDN上的文档 。

这完全取决于您将如何使用32位数字。

如果您想执行以下操作:

 i++; 

这隐含地分解成了

  1. 读到i的价值
  2. 加一个
  3. 存储i

如果另一个线程在1之后但在3之前修改i,那么你有一个问题,我是7,你添加一个,现在它是492。

但如果您只是阅读我,或执行单一操作,例如:

 i = 8; 

那你就不需要锁定我了。

现在,你的问题是,“……读取时需要锁定.NET Int32 ……”但是你的例子包括读取然后写入 Int32。

所以,这取决于你在做什么。

只有1个线程锁没有任何结果。 锁的目的是阻止其他线程,但如果没有其他人检查锁,它就不起作用!

现在,您不需要担心32位int的内存损坏,因为写入是primefaces的 – 但这并不一定意味着您可以无锁。

在您的示例中,可能会出现可疑的语义:

 example = 10 Thread A: Add(10) read example (10) Thread B: Read() read example (10) Thread A: write example (10 + 10) 

这意味着ThreadB 线程A开始更新开始读取示例的值 – 但是读取了更新后的值。 我认为,这是否是一个问题取决于这个代码应该做什么。

由于这是示例代码,因此可能很难看到问题。 但是,想象一下规范的反函数:

  class Counter { static int nextValue = 0; static IEnumerable GetValues(int count) { var r = Enumerable.Range(nextValue, count); nextValue += count; return r; } } 

然后,以下场景:

  nextValue = 9; Thread A: GetValues(10) r = Enumerable.Range(9, 10) Thread B: GetValues(5) r = Enumerable.Range(9, 5) nextValue += 5 (now equals 14) Thread A: nextValue += 10 (now equals 24) 

nextValue正确递增,但返回的范围将重叠。 从未返回19 – 24的值。 您可以通过锁定var r和nextValue赋值来解决此问题,以防止任何其他线程同时执行。

如果您需要锁定primefaces,则需要锁定。 读取和写入(作为配对操作,例如当您执行i ++时),由于缓存,32位数不能保证是primefaces的。 此外,个人读或写不一定适合登记(波动)。 如果你想要修改整数(例如读取,增量,写操作),那么使它变得不稳定并不能保证primefaces性。 对于整数,互斥锁或监视器可能太重(取决于您的用例),这就是Interlocked类的用途。 它保证了这些类型操作的primefaces性。

通常,只有在修改值时才需要锁定

编辑: 马克布拉克特的优秀总结更贴切:

“如果你希望非primefaces操作是primefaces的,则需要锁定”

在这种情况下,在32位机器上读取32位整数可能已经是primefaces操作……但也许不是! 也许volatile变量可能是必要的。