我们需要在多线程代码中读取.NET Int32时锁定它?

时间:2008-12-27 18:13:01

标签: c# .net multithreading locking

我正在阅读以下文章: 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提供)我理解上面的代码仍然容易受到多处理器缓存的影响(每个处理器都有自己的缓存,与其他处理器不同步)。所有这三个修改都解决了这个问题:

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

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

6 个答案:

答案 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++;

隐含地分解为

  1. 读取i
  2. 的值
  3. 添加一个
  4. 存储i
  5. 如果另一个线程在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关键字可能是必要的。