根据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); }
}
}
答案 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);
好的,现在让我们分开这个任务。涉及多个步骤。
BoxedT
分配内存。BoxedT.t
。value
成员
BoxedT
中的bar
的引用。步骤3的原子性意味着当您从bar
读取时,您将获得旧值或新值,而不是两者的混合。但它不能保证同步。特别地,在操作2之前,操作3可以对其他处理器可见。
假设bar
的更新对另一个处理器可见,但BoxedT.t
的初始化不可见。当该处理器尝试通过读取BoxedT
值来取消Boxed.t
的取消包装时,不能保证读取在步骤2中写入的t
的完整值。它可能只是值,另一部分包含default(T)
。
这与双重检查锁定模式基本相同,但更糟糕的是因为你完全没有锁定!解决方案是使用发布语义更新bar
,以便在bar
更新之前将所有先前的存储提交到内存。根据C#4语言规范10.5.3节,可以将bar
标记为volatile
来完成。 (这也意味着来自bar
的所有读取都将具有获取语义,这可能是您想要的,也可能不是。)