锁定语句的内存障碍

我最近读到了关于内存障碍和重新排序的问题,现在我对它有些困惑。

请考虑以下情形:

private object _object1 = null; private object _object2 = null; private bool _usingObject1 = false; private object MyObject { get { if (_usingObject1) { return _object1; } else { return _object2; } } set { if (_usingObject1) { _object1 = value; } else { _object2 = value; } } } private void Update() { _usingMethod1 = true; SomeProperty = FooMethod(); //.. _usingMethod1 = false; } 
  1. Update方法; 是获取或设置属性之前始终执行的_usingMethod1 = true语句? 或者由于重新订购问题我们无法保证?

  2. 我们应该使用volatile吗?

     private volatile bool _usingMethod1 = false; 
  3. 如果我们使用lock; 我们可以保证锁定中的每个语句都将按顺序执行,如:

     private void FooMethod() { object locker = new object(); lock (locker) { x = 1; y = a; i++; } } 

记忆障碍的主题非常复杂。 它甚至不时地让专家绊倒。 当我们谈论记忆障碍时,我们真的将两种不同的想法结合起来。

  • 获取栅栏:一种内存屏障,其中不允许其他读写操作在栅栏之前移动。
  • 释放栅栏:一种内存屏障,在栅栏不允许其他读写操作。

仅创建两个中的一个的内存屏障有时称为半栅栏 。 创建两者的内存屏障有时称为全栅栏

volatile关键字创建半栅栏。 易失性字段的读取具有获取语义,而写入具有释放语义。 这意味着在读取之前或写入之后不能移动指令。

lock关键字在两个边界(入口和出口)上创建全栅栏。 这意味着在每个边界之前或之后都不能移动指令。

但是,如果我们只关心一个线程,那么所有这些都没有实际意义。 由该线程感知的排序始终保留。 事实上,没有基本保证,任何计划都无法正常运作。 真正的问题是其他线程如何感知读写。 这是你需要关注的地方。

所以回答你的问题:

  1. 从单个线程的角度来看……是的。 从另一个线程的角度来看……没有。

  2. 这取决于。 这可能有用,但我需要更好地了解你想要实现的目标。

  3. 从另一个线程的角度来看……没有。 读取和写入可以在锁定边界内自由移动。 他们只是无法超越这些界限。 这就是为什么其他线程也必须创建内存障碍。

volatile关键字在这里没有完成任何事情。 它有很弱的保证,并不意味着存储障碍。 您的代码未显示另一个线程被创建,因此很难猜测是否需要锁定。 但是,如果两个线程可以同时执行Update()并使用相同的对象,则这是一个很难的要求。

请注意,您发布的锁定代码不会锁定任何内容。 每个线程都有自己的“locker”对象实例。 你必须使它成为你的类的私有字段,由构造函数或初始化程序创建。 从而:

 private object locker = new object(); private void Update() { lock (locker) { _usingMethod1 = true; SomeProperty = FooMethod(); //.. _usingMethod1 = false; } } 

请注意,SomeProperty分配也会有竞争。