是否通过 CALLED 方法中的 lock 语句保护通过 `ref` 参数访问字段?

use*_*740 4 c# ref thread-safety

鉴于,

private object _x;

private object LoadAndSet(ref object x) {
   // lock established over read and update
   lock (somePrivateObjectNotUsedElsewhereThatIsIrrelvantToTheQuestion) {
       if (x == null)
           x = Load();
       return x;
   }
}
Run Code Online (Sandbox Code Playgroud)

调用为,

public object Load() {
    return LoadAndSet(ref _x);
}
Run Code Online (Sandbox Code Playgroud)
  • 字段( )的读取/写入“通过引用传递”是否包含在 的原子性/可见性保证下?_xlock

也就是说,第一个代码等价于下面直接使用字段的地方吗?(赋值直接发生,而不是通过ref参数。)

private object _x;

private object LoadAndSetFieldDirectly() {
   // lock established over read and update
   lock (somePrivateObjectNotUsedElsewhereThatIsIrrelvantToTheQuestion) {
       if (_x == null)
           _x = Load();
       return _x;
   }
}

public object Load() {
    return LoadAndSetFieldDirectly();
}
Run Code Online (Sandbox Code Playgroud)

我怀疑这是真的由于使用的ldind.refstind.ref在方法的MSIL; 然而,问题是在编写此类ref代码时需要有关线程安全(或缺乏)的权威文档/信息。

Eri*_*ert 5

的语义lock(lockObject) { statements }是:

  • 没有两个不同的线程位于使用相同 lockObject 实例锁定的锁定代码区域中。如果一个线程在该区域中,那么它要么进入无限循环,要么正常完成,要么抛出。(或者,对于高级玩家,通过 进入等待状态Monitor.Wait;这不是关于正确使用监视器对象的教程。)在控制离开区域之前,第二个线程不会进入受保护区域。
  • 锁定中或锁定之后的变量读取不会“及时向后”移动到锁定之前。
  • 在锁定中或锁定之前写入的变量不会“及时向前”移动到锁定之后。

(这是一个快速的非正式摘要;有关 C# 规范保证读取和写入重新排序的确切细节,请参阅规范。)

无论锁体中访问的变量是字段、局部变量、普通形式参数、引用/输出形式参数、数组元素还是指针解引用,这些语义都由运行时强制执行。

也就是说,有三件事让我对你的代码感到紧张。

首先,对现有机制进行不必要且次优的重新发明。如果要实现延迟初始化,请使用Lazy<T>. 当您可以使用由已经充分发挥其性能的专家编写的代码时,为什么要自己动手并冒犯错的风险呢?

其次,您必须确保该字段的每次使用都处于锁定状态,而不仅仅是它的初始化。将字段作为ref参数传递会为该字段创建别名,现在您已经完成了验证是否在锁下更难使用该字段的工作。

第三,这里似乎有机会进行不必要的争论。如果两个不同的字段都由两个不同线程上的相同代码初始化怎么办?现在,当他们不需要时,他们正在争夺锁。