在C#中装箱/取消装箱结构可以产生与原子相同的效果吗?

时间:2015-09-23 00:03:48

标签: 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; }
            set { bar = value; }
        }
    }

    // var foo = new Foo<Baz>();

编辑:@vesan这不是Atomic Assignment of Reference Sized Structs的副本。这个问题要求装箱和拆箱的效果,而另一个是关于结构中的单个引用类型(不涉及装箱/拆箱)。这两个问题之间唯一的相似之处是结构和原子(你真的读过这个问题吗?)。

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

1 个答案:

答案 0 :(得分:5)

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

让我们把事情分开。盒装类型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. 使用BoxedT.t
  3. 的副本初始化value成员
  4. 保存对BoxedT中的bar的引用。
  5. 步骤3的原子性意味着当您从bar读取时,您将获得旧值或新值,而不是两者的混合。但它不能保证同步。特别地,在操作2之前,操作3可以对其他处理器可见。

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

    这与双重检查锁定模式基本相同,但更糟糕的是因为你完全没有锁定!解决方案是使用发布语义更新bar,以便在bar更新之前将所有先前的存储提交到内存。根据C#4语言规范10.5.3节,可以将bar标记为volatile来完成。 (这也意味着来自bar的所有读取都将具有获取语义,这可能是您想要的,也可能不是。)