为什么不';是否所有成员变量都需要volatile来确保线程安全,即使在使用Monitor时也是如此?(为什么这个

本文关键字:Monitor 为什么 安全 成员 是否 变量 确保 线程 volatile 为什么不 | 更新日期: 2023-09-27 18:21:43

这个问题源于没有volatile的编译器(理论上可以通过各种方式优化任何变量,包括将其存储在CPU寄存器中。)而文档表示,在使用同步(如锁定变量)时不需要这样做。但在某些情况下,编译器/jit似乎不可能知道您是否会在代码路径中使用它们。因此,人们怀疑这里确实发生了其他事情,使记忆模型"起作用"。

在这个例子中,是什么阻止编译器/jit将_count优化到寄存器中,从而在寄存器上完成增量,而不是直接写入内存(稍后在退出调用后写入内存)?如果_count是volatile,那么看起来一切都应该很好,但很多代码都是在没有volatile的情况下编写的。如果编译器在方法中看到锁或同步对象,那么它可以知道不优化寄存器中的_count。。但在这种情况下,锁调用在另一个函数中。

大多数文档都说,如果使用锁之类的同步调用,就不需要使用volatile。

那么,是什么阻止编译器将_count优化到寄存器中,并可能只更新锁中的寄存器呢?我有一种感觉,由于这个确切的原因,大多数成员变量都不会被优化到寄存器中,因为每个成员变量都需要是可变的,除非编译器可以告诉它不应该优化(否则我怀疑大量的代码会失败)。几年前,当我研究C++时,我看到了类似的东西,局部函数变量被存储在寄存器中,类成员变量没有。

所以主要的问题是,编译器/jit不会把类成员变量放在寄存器中,因此volatile就没有必要了,这真的是在没有volatile的情况下唯一可行的方法吗?

(请忽略呼叫中缺乏异常处理和安全性,但您了解要点。)

public class MyClass
{
  object _o=new object();
  int _count=0;
  public void Increment()
  {
    Enter();
    // ... many usages of count here...
    count++;
    Exit();
  }


//lets pretend these functions are too big to inline and even call other methods 
// that actually make the monitor call (for example a base class that implemented these) 
  private void Enter() { Monitor.Enter(_o); }  
  private void Exit()  { Monitor.Exit(_o); }  //lets pretend this function is too big to inline
// ...
// ...
}

为什么不';是否所有成员变量都需要volatile来确保线程安全,即使在使用Monitor时也是如此?(为什么这个

输入和离开Monitor会导致内存不足。因此,CLR确保Monitor.Enter/Monitor.Exit之前的所有写入操作对所有其他线程都可见,并且方法调用之后的所有读取操作都"发生"在它之后。这也意味着调用之前的语句不能在调用之后移动,反之亦然。

请参阅http://www.albahari.com/threading/part4.aspx.

这个问题的最佳答案似乎是,在调用任何函数之前,存储在CPU寄存器中的任何变量都会保存到内存中。这是有道理的,因为从单个线程的编译器设计角度来看需要这样做,否则,如果其他函数/方法/对象使用该对象,则该对象可能看起来不一致。因此,可能并不像一些人/文章所说的那样,同步对象/类是由编译器检测到的,非易失性变量通过它们的调用是安全的。(可能是在使用锁或同一方法中的其他同步对象时,但一旦在另一个方法中调用了调用这些同步对象的调用,则可能不会),相反,仅调用另一种方法就可能足以将存储在CPU寄存器中的值保存到内存中。因此,不需要所有变量都是可变的。

此外,我怀疑和其他人也怀疑,由于一些线程问题,类的字段没有得到优化。

一些注意事项(我的理解):Thread.MemoryBarrier()主要是一条CPU指令,以确保从CPU的角度来看,写入/读取不会绕过障碍。(这与存储在寄存器中的值没有直接关系)因此,这可能不是直接导致将变量从寄存器保存到内存的原因(除了根据我们在这里的讨论,它是一个方法调用,这可能会导致这种情况发生-它可能真的是任何方法调用,尽管可能会影响从寄存器保存的所有类字段)

从理论上讲,JIT/编译器也可以在同一方法中考虑该方法,以确保变量存储在CPU寄存器中。但是,只要遵循我们提出的对另一个方法或类的任何调用的简单规则,就会将存储在寄存器中的变量保存到内存中。此外,如果有人将该调用封装在另一个方法中(可能有许多方法是深度的),编译器就不太可能分析那么深来推测执行情况。JIT可以做一些事情,但它可能不会深入分析,而且这两种情况都需要确保锁/同步在任何情况下都能工作,因此最简单的优化是可能的答案。

除非我们有编写编译器的人能够证实这一点,否则这一切都是猜测,但这可能是我们对为什么不需要volatile的最佳猜测。

如果遵循该规则,同步对象只需要在进入和离开时使用自己对MemoryBarrier的调用,以确保CPU从其写缓存中具有最新的值,从而刷新它们,从而可以读取正确的值。在这个网站上,你会看到这就是所谓的内隐记忆障碍:http://www.albahari.com/threading/part4.aspx

那么,是什么阻止编译器将_count优化到寄存器中呢并且可能只更新锁中的寄存器?

据我所知,文件中没有任何内容可以阻止这种情况的发生。关键是对Monitor.Exit的调用将有效地保证_count的最终值将在完成时提交给内存。

编译器可能知道不将_count优化为如果在方法中看到锁定或同步对象,请注册。。但在这种情况下,锁调用在另一个函数中。

从您的角度来看,通过其他方法获取和释放锁这一事实是无关紧要的。模型内存定义了一组非常严格的规则,这些规则必须遵守内存屏障生成器。将这些Monitor调用放在另一个方法中的唯一结果是JIT编译器将更难遵守这些规则。但是,JIT编译器必须遵守;时期如果方法调用变得复杂或嵌套太深,那么我怀疑JIT编译器在这方面可能会采用任何启发式方法,并说:"算了,我不会优化任何东西!"

所以主要的问题是,这真的是唯一可行的方法吗如果没有volatile,编译器/jit就不会放入类成员那么寄存器中的变量和易失性是不必要的吗?

它之所以有效,是因为协议也是在读取_count之前获取锁。如果读者不这样做,那么所有的赌注都会被取消。