Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/.net/25.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 在多线程代码中读取.NET Int32时,我们需要锁定它吗?_C#_.net_Multithreading_Locking - Fatal编程技术网

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

C# 在多线程代码中读取.NET Int32时,我们需要锁定它吗?,c#,.net,multithreading,locking,C#,.net,Multithreading,Locking,我正在读以下文章: Joe Duffy的“解决多线程代码中可能出现的11个问题” 这给我提出了一个问题: “在多线程代码中读取.NET Int32时,需要锁定它吗?” 我理解,如果它是32位的Int64,那么它可能会撕裂,正如文章中所解释的那样。但对于Int32,我设想了以下情况: class Test { private int example = 0; private Object thisLock = new Object(); public void Add(int ano

我正在读以下文章: Joe Duffy的“解决多线程代码中可能出现的11个问题”

这给我提出了一个问题: “在多线程代码中读取.NET Int32时,需要锁定它吗?”

我理解,如果它是32位的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编写),我了解到上面的代码仍然容易受到多处理器缓存的攻击(每个处理器都有自己的缓存,与其他处理器不同步)。下面的三个修改都解决了问题:

  • 向“int-example”添加“volatile”属性
  • 插入线程。MemoryBarrier();在实际读取“int-example”之前
  • 阅读“锁(thisLock)”中的“int-example”

  • 我还认为“volatile”是最优雅的解决方案。

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

    编辑:'的优秀总结更贴切:“当您希望非原子操作是原子操作时,需要锁”


    在这种情况下,在32位机器上读取32位整数可能已经是一个原子操作。。。但也许不是!也许这个关键字是必要的。

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

    如果要执行以下操作:

    i++;
    
    这隐含地分解为

  • 读取
    i的值
  • 添加一个
  • 存储
    i
  • 如果另一个线程在1之后但在3之前修改i,那么在我7的地方有一个问题,添加一个,现在是492

    但是如果你只是在读i,或者执行一个操作,比如:

    i = 8;
    
    那你就不需要锁我了

    现在,您的问题是,“…在阅读.NET Int32时需要锁定它…” 但您的示例涉及到读取然后写入Int32


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

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

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

    在您的示例中,可能会得到有问题的语义:

    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<int> GetValues(int count) {
           var r = Enumerable.Range(nextValue, count);
           nextValue += count;
           return r;
        }
     }
    

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

    锁定完成两件事:

    • 它充当互斥体,因此您可以确保一次只有一个线程修改一组值
    • 它提供内存屏障(获取/释放语义),确保一个线程的内存写入在另一个线程中可见
    大多数人理解第一点,但不理解第二点。假设您在问题中使用了来自两个不同线程的代码,一个线程反复调用
    Add
    ,另一个线程调用
    Read
    。原子性本身将确保您只读取8的倍数-如果有两个线程调用
    Add
    ,那么锁将确保您不会“丢失”任何添加。但是,您的
    Read
    线程很可能只读取0,即使在多次调用
    Add
    之后也是如此。在没有任何内存障碍的情况下,JIT可以将值缓存在寄存器中,并假设在读取之间没有更改。内存屏障的作用是确保某些内容确实写入了主内存,或者确实从主内存读取


    内存模型可能会变得非常复杂,但是如果你遵循一个简单的规则,每次你想要访问共享数据(读或写)时都要打开一个锁,那么你就没事了。有关更多详细信息,请参阅我的线程教程部分。

    这取决于上下文。处理整型或引用时,您可能希望使用System.Threading.Interlocked类的成员

    典型用法如下:

    if( x == null )
      x = new X();
    
    可以替换为对联锁的调用。CompareExchange()

    Interlocked.CompareExchange( ref x, new X(), null);
    
    compareeexchange()保证比较和交换作为原子操作进行


    联锁类的其他成员,如添加()减量()交换()增量()读取()都以原子方式执行各自的操作。请阅读MSDN上的说明。

    如果您需要它是原子的,那么锁定是必要的。由于缓存的原因,32位数字的读写(作为配对操作,例如在执行i++时)不能保证是原子的。此外,单个读或写不一定直接进入寄存器(波动性)。如果您希望修改整数(例如,读取、递增、写入操作),那么将其设置为volatile并不能保证原子性。对于整数,互斥锁或监视器可能太重(取决于您的用例),这就是它的用途。它保证了这些类型操作的原子性。

    。如果希望非原子操作是原子操作,则需要锁。由于对复杂类型的写入不是原子的,因此需要一个锁,但也有许多其他场景需要锁。@@[Mark Brackett]:true
    Interlocked.CompareExchange( ref x, new X(), null);