我正在阅读以下文章: http://msdn.microsoft.com/en-us/magazine/cc817398.aspx “解决你的多线程代码中的11个可能的问题”作者:Joe Duffy
它提出了一个问题: “在使用多线程代码读取.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提供)我理解上面的代码仍然容易受到多处理器缓存的影响(每个处理器都有自己的缓存,与其他处理器不同步)。所有这三个修改都解决了这个问题:
而且我也认为“易变”是最优雅的解决方案。
答案 0 :(得分:24)
锁定完成两件事:
大多数人都了解第一点,但不是第二点。假设您使用了来自两个不同线程的问题中的代码,其中一个线程重复调用Add
,另一个线程调用Read
。原子性本身将确保您最终只读取8的倍数 - 如果有两个线程调用Add
,您的锁将确保您没有“丢失”任何添加。但是,即使在多次调用Read
之后,您的Add
线程也很可能只读0。没有任何内存障碍,JIT可以将值缓存在寄存器中,并假设它在读取之间没有变化。内存障碍的关键是确保某些内容真正写入主内存,或者真正从主内存中读取。
内存模型可能非常繁琐,但如果您按照每次要访问共享数据时取出锁定的简单规则(对于读取或写入),您就可以了。有关详细信息,请参阅我的线程教程的volatility/atomicity部分。
答案 1 :(得分:7)
这完全取决于具体情况。处理整数类型或引用时,您可能希望使用 System.Threading.Interlocked 类的成员。
典型用法如:
if( x == null )
x = new X();
可以通过调用 Interlocked.CompareExchange()来替换:
Interlocked.CompareExchange( ref x, new X(), null);
Interlocked.CompareExchange()保证比较和交换作为原子操作发生。
Interlocked类的其他成员,例如添加(),减少(), Exchange(),增量( )和 Read()都以原子方式执行各自的操作。阅读MSDN上的documentation。
答案 2 :(得分:3)
这完全取决于您将如何使用32位数字。
如果您想执行以下操作:
i++;
隐含地分解为
i
i
如果另一个线程在1之后但在3之前修改i,那么你有一个问题,我是7,你添加一个,现在它是492。
但是,如果您只是简单地阅读i,或执行单一操作,例如:
i = 8;
然后你不需要锁定我。
现在,您的问题是,“......在阅读时需要锁定.NET Int32 ......” 但是你的例子涉及阅读然后将写入Int32。
所以,这取决于你在做什么。
答案 3 :(得分:2)
只有1个线程锁完成任何事情。锁定的目的是阻止其他线程,但如果没有其他人检查锁定它就不起作用!
现在,您不必担心使用32位int导致内存损坏,因为 write 是原子的 - 但这并不一定意味着您可以无锁定。< / p>
在您的示例中,可能会出现可疑的语义:
example = 10
Thread A:
Add(10)
read example (10)
Thread B:
Read()
read example (10)
Thread A:
write example (10 + 10)
这意味着在线程A开始更新后,ThreadB开始读取示例的值 - 但是读取了更新后的值。这是否是一个问题取决于这个代码应该做什么,我想。
由于这是示例代码,因此可能很难看到问题。但是,想象一下规范的反函数:
class Counter {
static int nextValue = 0;
static IEnumerable<int> 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赋值来解决这个问题,以防止任何其他线程同时执行。
答案 4 :(得分:2)
如果您需要锁定原子,则需要锁定。读取和写入(作为配对操作,例如当您执行i ++时)32位数字不由于缓存而保证是原子的。此外,个人读或写不一定适合登记(波动)。如果你想要修改整数(例如读取,增量,写操作),那么使它变得不稳定并不能保证原子性。对于整数,互斥锁或监视器可能太重(取决于您的使用情况),这就是Interlocked class的用途。它保证了这些类型操作的原子性。
答案 5 :(得分:0)
通常,只有在修改值时才需要锁定
编辑:Mark Brackett的优秀摘要更贴切:
“如果您希望非原子操作是原子的,则需要锁定”
< / p>
在这种情况下,在32位机器上读取32位整数可能已经原子操作......但可能不是!也许volatile关键字可能是必要的。