是否有可能观测到来自另一个线程的局部构造的对象?有可能、线程、局部、对象

2023-09-02 02:08:03 作者:傲世,小狂徒

我常常听到,在.NET 2.0的内存模型,写始终使用的释放栅栏。这是真的?这是否意味着,即使没有明确的内存屏障或锁定,就不可能观察到部分构造的对象的线程从它创建一个不同的(只考虑引用类型)?我显然不计的情况下构造漏引用。

例如,假设我们有不可改变的引用类型:

 公共类Person
{
    公共字符串名称{;私定; }
    公众诠释年龄{获得;私定; }

    公众人物(字符串名称,诠释年龄)
    {
        名称=名称;
        年龄=岁;
    }
}
 

有没有可能与下列code观察比约翰·20和杰克·21之外的任何输出,说空20或杰克0?

  //我们可以让这个动荡梳洗读,但我不希望
//为复杂问题的核心。
私募人士的人;

私人无效线程1()
{
    而(真)
    {
        VAR personCopy =人;

        如果(personCopy!= NULL)
            Console.WriteLine(personCopy.Name ++ personCopy.Age);
    }
}

私人无效线程2()
{
    VAR随机=新的随机();

    而(真)
    {
        人= random.Next(2)== 0
            ?新的人(约翰福音,20)
            新的人(杰克,21);
    }
}
 
关于多线程操作的几个方法

这是否也意味着,我可以让深层不可变的引用类型挥发性和(在大多数情况下),只是让我的工作,所有的共享领域?

解决方案   

我常常听到,在.NET 2.0的内存模型,写始终使用   释放栅栏。这是真的吗?

这要看你指的是什么型号的。

首先,让我们precisely定义释放围栏屏障。发布语义规定,没有其他的读取或写入之前出现在指令序列中的屏障允许的障碍后移动。

的ECMA规范有一个轻松的模型,其中写道:不提供这样的保证。 在它被引用的地方,由微软提供的CLR实现加强了模型制作写有释放围栏语义。 x86和x64体系结构增强模式通过写入释放围栏障碍和读取采集围栏障碍。

所以有可能是另一种实现的CLI(如单声道)在一个深奥的架构上运行(如ARM的Windows 8将现在的目标)将不可以提供释放围栏语义上写。请注意,我说这是可能的,但不能肯定。但是,所有的游戏存储模型,如不同的软件和硬件层之间,你必须$ C $下最弱的模型,如果你希望你的code是真正的便携。这意味着编码对ECMA模型,并没有做任何假设。

我们应该在比赛的内存模型层的名单仅仅是明确的。

编译器:在C#(或VB.NET或其他)可以移动的指令 运行:通过JIT编译器显然,CLI运行时可以移动的指令 硬件:当然,CPU和内存架构进场以及   

这是否意味着,即使没有明确的内存屏障或锁,它   是,没有观察到的局部构造的对象(考虑   在一个线程只引用类型)从一个不同的其   创建?

是(合格):如果该应用程序运行的环境是晦涩不够那么有可能对从另一个线程观察到部分构造实例。这就是为什么双重检查锁定模式将是不安全的,而无需使用挥发性。但在现实中,我怀疑你会不会遇到这个主要是因为微软的执行CLI不会重新排序以这种方式说明。

  

有没有可能与下列code观察到任何输出   除了约翰·20和杰克·21,说空20或杰克0?

再有,就是合格的肯定。但对于某些原因,如上面我怀疑你永远不会看到这种行为。

不过,我要指出的是,因为未标记为挥发性这可能是可能的,什么都不是印刷,因为在所有读线程可以一直把它看作。但在现实中,我敢打赌, Console.WriteLine 调用将导致C#和的JIT编译器,以避免可能以其他方式移动的读解除操作人外循环。我怀疑你已经清楚知道这个细微差别已经是。

  

这是否也意味着,我可以做的所有共享领域   深层不可变的引用类型挥发,(在大多数情况下)获得   我的工作?

我不知道。这是一个pretty的加载的问题。我不舒服回答两种方式没有更好的理解其背后的语境。我能说的是,我通常有利于更多的外显记忆的指令避免使用挥发性互锁操作, Thread.VolatileRead Thread.VolatileWrite Thread.MemoryBarrier 。再说,我也尽量完全避免有利于更高级别的同步机制,如锁没锁code

更新:

我喜欢想象的事情的一种方式是假设C#编译器,JITer等,将尽最大可能地优化。这意味着, Person.ctor 可能是一名候选人为内联(因为它是简单的),这将产生以下伪code。

 人员参考=分配空间的人
ref.Name =名称;
ref.Age =年龄;
人=实例;
DoSomething的(人);
 

而因为写有在ECMA规范没有释放栅栏语义那么其他的读取和放大器;写可以浮动倒过去的分配产生下面的指令有效序列。

 人员参考=分配空间的人
人= REF;
person.Name =名称;
person.Age =年龄;
DoSomething的(人);
 

因此​​,在这种情况下,你可以看到被分配在初始化之前。这是有效的,因为从执行线程的透视的逻辑顺序保持与物理序列一致。有没有意外的副作用。但是,其原因应该是显而易见的,这个序列将是灾难性的另一个线程。

I've often heard that in the .NET 2.0 memory model, writes always use release fences. Is this true? 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? I'm obviously excluding cases where the constructor leaks the this reference.

For example, let's say we had the immutable reference type:

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;
    }
}

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" ?

// 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);
    }
}

Does this also mean that I can make all shared fields of deeply-immutable reference-types volatile and (in most cases) just get on with my work?

解决方案

I've often heard that in the .NET 2.0 memory model, writes always use release fences. Is this true?

It depends on what model you are referring to.

First, let us precisely define a release-fence barrier. Release semantics stipulate that no other read or write appearing before the barrier in the instruction sequence is allowed to move after that barrier.

The ECMA specification has a relaxed model in which writes do not provide this guarantee. It has been cited somewhere that the CLR implementation provided by Microsoft strengthens the model by making writes have release-fence semantics. The x86 and x64 architectures strengthen the model by making writes release-fence barriers and reads acquire-fence barriers.

So it is possible that another implementation of the CLI (such as Mono) running on an esoteric architecture (like ARM which Windows 8 will now target) would not provide release-fence semantics on writes. Notice that I said it is possible, but not certain. But, between all of the memory models in play, such as the different software and hardware layers, you have to code for the weakest model if you want your code to be truly portable. That means coding against the ECMA model and not making any assumptions.

We should make a list of the memory model layers in play just be explicit.

Compiler: The C# (or VB.NET or whatever) can move instructions. Runtime: Obviously the CLI runtime via the JIT compiler can move instructions. Hardware: And of course the CPU and memory architecture comes into play as well.

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?

Yes (qualified): If the environment in which the application is running is obscure enough then it might be possible for a partially constructed instance to be observed from another thread. This is one reason why double-checked locking pattern would be unsafe without using volatile. In reality, however, I doubt you would ever run into this mostly because Microsoft's implementation of the CLI will not reorder instructions in this manner.

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" ?

Again, that is qualified yes. But for the some reason as above I doubt you will ever observe such behavior.

Though, I should point out that because person is not marked as volatile it could be possible that nothing is printed at all because the reading thread may always see it as null. In reality, however, I bet that Console.WriteLine call will cause the C# and JIT compilers to avoid the lifting operation that might otherwise move the read of person outside the loop. I suspect you are already well aware of this nuance already.

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?

I do not know. That is a pretty loaded question. I am not comfortable answering either way without a better understanding of the context behind it. What I can say is that I typically avoid using volatile in favor of more explicit memory instructions such as the Interlocked operations, Thread.VolatileRead, Thread.VolatileWrite, and Thread.MemoryBarrier. Then again, I also try to avoid no-lock code altogether in favor of the higher level synchronization mechanisms such as lock.

Update:

One way I like visualize things is to assume that the C# compiler, JITer, etc. will optimize as aggressively as possible. That means that Person.ctor might be a candidate for inlining (since it is simple) which would yield the following pseudocode.

Person ref = allocate space for Person
ref.Name = name;
ref.Age = age;
person = instance;
DoSomething(person);

And because writes have no release-fence semantics in the ECMA specification then the other reads & writes could "float" down past the assignment to person yielding the following valid sequence of instructions.

Person ref = allocate space for Person
person = ref;
person.Name = name;
person.Age = age;
DoSomething(person);

So in this case you can see that person gets assigned before it is initialized. This is valid because from the perspective of the executing thread the logical sequence remains consistent with the physical sequence. There are no unintended side-effects. But, for reasons that should be obvious, this sequence would be disastrous to another thread.