在C#中装箱/拆箱结构是否能产生与原子结构相同的效果?

在C#中装箱/拆箱结构是否能产生与原子结构相同的效果?,c#,multithreading,boxing,C#,Multithreading,Boxing,根据C#规范,是否有任何保证保证foo.Bar将具有相同的原子效果(即,从不同线程读取foo.Bar时,在不同线程写入时不会看到部分更新的结构) 我一直认为是的。如果确实如此,我想知道规范是否保证了这一点 public class Foo<T> where T : struct { private object bar; public T Bar { get { return (T) bar;

根据C#规范,是否有任何保证保证
foo.Bar
将具有相同的原子效果(即,从不同线程读取
foo.Bar
时,在不同线程写入时不会看到部分更新的结构)

我一直认为是的。如果确实如此,我想知道规范是否保证了这一点

    public class Foo<T> where T : struct
    {
        private object bar;

        public T Bar
        {
            get { return (T) bar; }
            set { bar = value; }
        }
    }

    // var foo = new Foo<Baz>();
公共类Foo其中T:struct
{
私有对象栏;
公共酒吧
{
获取{return(T)bar;}
设置{bar=value;}
}
}
//var foo=new foo();
编辑:@vesan这不是的副本。这个问题询问装箱和取消装箱的效果,而另一个问题是关于结构中的单个引用类型(不涉及装箱/取消装箱)。这两个问题之间唯一的相似之处是单词struct和atomic(你真的读过这个问题吗?)

编辑2:以下是基于陈雷蒙回答的原子版本:

public class Atomic<T> where T : struct
{
    private object m_Value = default(T);

    public T Value
    {
        get { return (T) m_Value; }
        set { Thread.VolatileWrite(ref m_Value, value); }
    }
}
公共类原子,其中T:struct
{
私有对象m_值=默认值(T);
公共价值
{
获取{返回(T)m_值;}
设置{Thread.VolatileWrite(ref m_Value,Value);}
}
}
编辑3:4年后再看这个。事实证明,CLR2.0+的内存模型表明,
所有写入都具有易失性写入的效果:

因此,这个问题的答案应该是“如果硬件不重新排序写入,那么它就是原子的”,而不是雷蒙德的答案。JIT和编译器无法对写入进行重新排序,因此基于Raymond答案的“原子版本”是多余的。在弱内存模型体系结构上,硬件可能会重新排序写操作,因此需要适当的获取/释放语义


EDIT4:这个问题再次归结为CLR vs CLI(ECMA),后者定义了一个非常弱的内存模型,而前者实现了一个强内存模型。但是,无法保证运行时能够做到这一点,因此答案仍然成立。然而,由于绝大多数代码都是为CLR编写的,因此我怀疑任何试图创建新运行时的人都会选择更简单的方法来实现强内存模型,而损害性能(这只是我自己的观点)。

不,结果不是原子性的。虽然对引用的更新确实是原子性的,但它不是同步的。可以在装箱对象内的数据可见之前更新引用

让我们把东西拆开。装箱类型
T
基本上是这样的:

class BoxedT
{
    T t;
    public BoxedT(T value) { t = value; }
    public static implicit operator T(BoxedT boxed) { return boxed.t; }
}
(不完全正确,但就本次讨论而言,距离足够近。)

当你写作时

bar = value;
这是英语的简写

bar = new BoxedT(value);
好的,现在让我们把这个作业分开。这涉及多个步骤

  • BoxedT
    分配内存
  • 使用
    值的副本初始化
    BoxedT.t
    成员
  • 栏中保存对
    BoxedT
    的引用
  • 步骤3的原子性意味着当您从
    读取时,您将获得旧值或新值,而不是两者的混合。但它不能保证同步。具体而言,在操作2之前,操作3可能对其他处理器可见

    假设另一个处理器可以看到
    bar
    的更新,但是
    BoxedT.t
    的初始化不可见。当该处理器试图通过读取
    Boxed.t
    值来取消对
    BoxedT
    的装箱时,不能保证读取在步骤2中写入的
    t
    的完整值。它可能只获取部分值,而另一部分包含
    default(T)


    这与双重检查锁定模式基本相同,但更糟糕的是,您根本没有锁!解决方案是使用release语义更新
    bar
    ,以便在更新
    bar
    之前将所有以前的存储提交到内存中。根据C#4语言规范第10.5.3节,这可以通过将
    bar
    标记为
    volatile
    来实现。(这也意味着从
    bar
    读取的所有内容都将具有acquire语义,这可能是您想要的,也可能不是您想要的。)

    可能重复@vesan它不是重复的-您似乎根本不理解这个问题。@vesan我认为这不是正确的重复,因为这个问题是关于取消结构的原子化或非原子化,而建议的副本是关于整个结构只是引用。答案可能是同一个规范,但有不同的实现。没错,问题是不同的,但链接问题的答案列出了相关文档,应该有助于回答这个问题。当然,请随意不同意:)@vesan继续,然后为这个问题提供答案。答案很好。这正是我想知道的!顺便说一句,在一个具有强内存模型的平台上,将
    bar
    标记为
    volatile
    在这种情况下不应该有任何效果,因为存储总是按顺序提交的(例如x86/x64),对吗?另外,
    不会设置{Thread.VolatileWrite(ref bar,value);}
    这样做,但会阻止获取
    get
    的语义吗?如果这不是比将
    bar
    标记为
    volatile
    ?@ZachSaw更好的实现,那么即使在具有强内存模型的平台上,您也需要进行适当的标记,以防止编译器对写入进行重新排序,我只是假设步骤2-3不涉及编译器,即JIT总是为速记生成一组操作码,以便为强内存模型平台进行写操作,因为其他任何操作都会降低效率。我知道这当然不能保证,但我想提出的一点是,如果JIT的行为与我所描述的一样(很可能是这样),那么这种模式在x86/x64平台上就会出现原子化。