我经常听说在 .NET 2.0 内存模型中,写入总是使用释放栅栏。这是真的?这是否意味着即使没有明确的内存屏障或锁,也不可能在与创建对象不同的线程上观察到部分构造的对象(仅考虑引用类型)?我显然排除了构造函数泄漏 this
的情况。引用。
例如,假设我们有不可变的引用类型:
public class Person
{
public string Name { get; private set; }
public int Age { get; private set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
是否可以使用以下代码观察除“John 20”和“Jack 21”以外的任何输出,例如“null 20”或“Jack 0”?
// We could make this volatile to freshen the read, but I don't want
// to complicate the core of the question.
private Person person;
private void Thread1()
{
while (true)
{
var personCopy = person;
if (personCopy != null)
Console.WriteLine(personCopy.Name + " " + personCopy.Age);
}
}
private void Thread2()
{
var random = new Random();
while (true)
{
person = random.Next(2) == 0
? new Person("John", 20)
: new Person("Jack", 21);
}
}
这是否也意味着我可以创建深度不可变引用类型的所有共享字段
volatile
并且(在大多数情况下)继续我的工作?
最佳答案
I've often heard that in the .NET 2.0 memory model, writes always use release fences. Is this true?
这取决于您指的是什么型号。
首先,让我们精确定义一个释放栅栏。释放语义规定在指令序列中出现在屏障之前的任何其他读或写都不允许在该屏障之后移动。
因此,在深奥的体系结构(如 Windows 8 现在将针对的 ARM)上运行的 CLI 的另一种实现(如 Mono)可能会不是 在写入时提供释放栅栏语义。请注意,我说这是可能的,但不确定。但是,在所有正在使用的内存模型之间,例如不同的软件和硬件层,如果您希望代码真正具有可移植性,您必须为最弱的模型进行编码。这意味着针对 ECMA 模型进行编码,而不是做出任何假设。
我们应该明确列出正在运行的内存模型层。
Does this mean that even without explicit memory-barriers or locks, it is impossible to observe a partially-constructed object (considering reference-types only) on a thread different from the one on which it is created?
是(合格):如果应用程序运行的环境足够模糊,那么可能会从另一个线程观察到部分构造的实例。这就是为什么在不使用
volatile
的情况下双重检查锁定模式不安全的原因之一。 .然而,实际上,我怀疑您是否会遇到这种情况,主要是因为 Microsoft 的 CLI 实现不会以这种方式重新排序指令。Would it be possible with the following code to observe any output other than "John 20" and "Jack 21", say "null 20" or "Jack 0" ?
再次,这是合格的。但是由于上述某些原因,我怀疑您是否会观察到这种行为。
不过,我应该指出,因为
person
未标记为 volatile
可能根本没有打印任何内容,因为阅读线程可能总是将其视为 null
.然而,实际上,我敢打赌 Console.WriteLine
调用将导致 C# 和 JIT 编译器避免提升操作,否则可能会移动 person
的读取。在循环之外。我怀疑您已经很清楚这种细微差别。Does this also mean that I can just make all shared fields of deeply-immutable reference-types volatile and (in most cases) get on with my work?
我不知道。这是一个非常重要的问题。如果没有更好地了解其背后的背景,我不喜欢回答任何一种方式。我可以说的是,我通常避免使用
volatile
支持更明确的内存指令,例如 Interlocked
操作,Thread.VolatileRead
, Thread.VolatileWrite
, 和 Thread.MemoryBarrier
.再说一次,我也尝试完全避免无锁代码,以支持更高级别的同步机制,例如 lock
.更新:
我喜欢将事物可视化的一种方式是假设 C# 编译器、JITer 等将尽可能积极地优化。这意味着
Person.ctor
可能是内联的候选者(因为它很简单),这将产生以下伪代码。Person ref = allocate space for Person
ref.Name = name;
ref.Age = age;
person = instance;
DoSomething(person);
并且因为写入在 ECMA 规范中没有释放栅栏语义,所以其他读取和写入可以“ float ”到分配给
person
之后。产生以下有效的指令序列。Person ref = allocate space for Person
person = ref;
person.Name = name;
person.Age = age;
DoSomething(person);
所以在这种情况下你可以看到
person
在初始化之前被分配。这是有效的,因为从执行线程的角度来看,逻辑顺序与物理顺序保持一致。没有意外的副作用。但是,出于显而易见的原因,这个序列对另一个线程来说是灾难性的。
关于c# - 是否可以从另一个线程观察部分构造的对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8358707/