通过 CALL 方法中的 lock 语句访问字段是否由 REFERENCE 方法中的 lock 语句保护?



给定,

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

调用为,

public object Load() {
return LoadAndSet(ref _x);
}
读取
  • /写入字段(_x(是否"通过引用传递"包含在lock的原子性/可见性保证范围内?

也就是说,直接使用该字段的第一个代码是否等同于以下内容?(赋值直接发生,而不是通过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();
}

我怀疑这是真的,因为在方法的 MSIL 中使用了ldind.refstind.ref;但是,在编写此类ref代码时,问题是乞求有关线程安全(或缺乏(的权威文档/信息。

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

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

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

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

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

首先,这是对现有机制的不必要和次优的重新发明。如果要实现延迟初始化,请使用Lazy<T>。 为什么要自己动手并冒着出错的风险,当您可以使用已经从中勉强获得每一点性能的专家编写的代码时?

其次,您必须确保字段的每次使用都在锁定之下,而不仅仅是其初始化。 将字段作为ref参数传递会为该字段创建别名,现在您已经使验证是否在锁定下获取字段的所有用法的工作变得更加困难

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

最新更新