在c#中对一个结构体进行装箱/拆箱是否能产生与原子结构体相同的效果?

本文关键字:结构体 是否 一个 | 更新日期: 2023-09-27 18:14:17

根据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>();

编辑:@vesan这不是引用大小结构的原子赋值的副本。这个问题问的是装箱和拆箱的效果,而另一个问题是关于结构体中的单个引用类型(不涉及装箱/拆箱)。这两个问题之间唯一的相似之处是struct和atomic(你真的读过这个问题吗?)

EDIT2:以下是基于Raymond Chen回答的原子版本:

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

编辑3:时隔4年重访。结果表明,CLR2.0+的内存模型表明All writes have the effect of volatile write: https://blogs.msdn.microsoft.com/pedram/2007/12/28/clr-2-0-memory-model/

因此,这个问题的答案应该是"如果硬件不重新排序写入,它原子的",而不是Raymond的答案。JIT和编译器不能重新排序写入,因此基于Raymond的答案的"原子版本"是多余的。在弱内存模型体系结构中,硬件可能会重新排序写操作,因此您需要适当的获取/释放语义。

EDIT4:同样,这个问题归结为CLR与CLI (ECMA),后者定义了一个非常弱的内存模型,而前者实现了一个强内存模型。但不能保证运行时会这样做,所以答案仍然成立。然而,由于绝大多数代码都是为CLR编写的,我怀疑任何试图创建新运行时的人都会选择更容易的路径,并以牺牲性能为代价实现强内存模型(这只是我自己的观点)。

在c#中对一个结构体进行装箱/拆箱是否能产生与原子结构体相同的效果?

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

让我们把东西拆开。盒装类型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);

好了,现在我们把这个作业拆开。这涉及多个步骤。

  1. BoxedT分配内存
  2. value的副本初始化BoxedT.t成员
  3. bar中保存BoxedT的引用。

步骤3的原子性意味着当您从bar读取时,您将获得旧值或新值,而不是两者的混合。但是它不能保证同步。特别是,操作3可以在操作2之前被其他处理器看到。

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

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