在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/
EDIT4:同样,这个问题归结为CLR与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
分配内存 - 用
value
的副本初始化BoxedT.t
成员 - 在
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
的读取都将具有获取语义,这可能是您想要的,也可能不是。)