我是一个修补匠 - 毫无疑问。出于这个原因(并且除此之外),我最近做了一个小实验,以确认我怀疑写入struct
不是原子操作,意味着所谓的试图强制执行某些约束的“不可变”值类型可能会假设其目标失败。
我使用以下类型作为插图编写了a blog post about this:
struct SolidStruct
{
public SolidStruct(int value)
{
X = Y = Z = value;
}
public readonly int X;
public readonly int Y;
public readonly int Z;
}
虽然上面的看起来类似于X != Y
或Y != Z
永远不会出现的类型,但事实上这个可以发生值是“mid-assignment”,同时由一个单独的线程复制到另一个位置。
Z
而只是坚持X
和Y
怎么办?那只是64位;应该可以一步覆盖它们。
果然,它奏效了。(我知道你们当中有些人现在正在皱起眉头,想着,是的,呃。这怎么会有趣?幽默我。)当然,我不知道这是否有保证给我的系统。我对寄存器,缓存未命中等几乎一无所知(我实际上只是在不理解其含义的情况下反驳了我听过的术语);所以现在这对我来说都是一个黑盒子。
接下来我尝试了 - 仅仅是预感 - 是一个由32位使用2个short
字段组成的结构。这似乎也表现出“原子可分配性”。但然后我尝试了一个24位结构,使用了3个byte
字段:没有去。
突然,结构似乎再次受到“中期任务”副本的影响。
低至16位,带有2个byte
字段:原子再次!
有人可以向我解释为什么会这样吗?我听说过“比特打包”,“缓存线跨越”,“对齐”等等 - 但是我再也不知道这意味着什么,也不知道它是否与此相关。但我感觉就像我看到一个模式,却无法确切地说出它是什么;清晰度将不胜感激。
答案 0 :(得分:14)
您正在寻找的模式是CPU的原生字大小。
从历史上看,x86系列本身使用16位值(之前是8位值)。因此,您的CPU可以原子地处理这些:它是设置这些值的单个指令。
随着时间的推移,本机元素大小增加到32位,后来增加到64位。在每种情况下,都添加了一条指令来处理这个特定的位数。但是,为了向后兼容,旧的指令仍然存在,因此您的64位处理器可以使用所有以前的原始大小。
由于struct元素存储在连续的内存中(没有填充,即空的空间),运行时可以利用这些知识仅为这些大小的元素执行该单个指令。简而言之,这会产生您所看到的效果,因为CPU一次只能执行一条指令(尽管我不确定在多核系统上是否可以保证真正的原子性)。
但是,原生元素大小从不是24位。因此,没有单个指令可以写入24位,因此需要多个指令,并且会失去原子性。
答案 1 :(得分:5)
C#标准(ISO 23270:2006,ECMA-334)有关原子性的说法:
12.5变量引用的原子性 以下数据类型的读写应为原子:bool,char,byte,sbyte,short,ushort, uint,int,float和引用类型。此外,读取和写入具有基础类型的枚举类型 在前面的列表中也应该是原子的。 读取和写入其他类型,包括long,ulong,double, 和十进制,以及用户定义的类型,不一定是原子的。(强调我的)除了设计的库函数 为此目的,不保证原子读 - 修改 - 写,例如在增量或 减量。您的示例
X = Y = Z = value
是3个单独的赋值操作的简写,每个操作都被12.5定义为原子。 3个操作的顺序(将value
分配给Z
,将Z
分配给Y
,将Y
分配给X
) not 保证是原子的。
由于语言规范不要求原子性,而X = Y = Z = value;
可能是原子操作,无论它是否依赖于一大堆因素:
有人可能还会注意到,即使单个机器指令也不一定是原子操作 - 许多都是可中断的。
此外,访问CLI标准(ISO 23217:2006),我们发现第12.6.6节:
12.6.6原子读写 符合标准的CLI应保证正确的读写访问权限 对齐的内存位置不大于本机字大小(类型的大小 当对位置的所有写访问都是时,
native int
)是原子的(参见§12.6.2) 大小相同。原子写入除了写入之外不得改变任何位。除非 显式布局控制(参见分区II(控制实例布局))用于 改变默认行为,数据元素不大于自然字大小(native int
)的大小应正确对齐。对象引用应予以对待 好像它们以原始字大小存储。[ 注意:无法保证 关于内存的原子更新(读 - 修改 - 写),除了提供的方法 该目的是作为类库的一部分(参见Partition IV)。(强调我的) 原子写入“小数据项”(不大于本机字大小的项) 需要在不支持direct的硬件上进行原子读取/修改/写入 写入小数据项。 结束记录]
[注意:当大小为0时,没有保证对8字节数据的原子访问 即使某些实现可能执行原子,本机int也是32位 数据在8字节边界上对齐时的操作。 结束记录]
答案 2 :(得分:3)
x86 CPU操作以8位,16位,32位或64位进行;操纵其他尺寸需要多次操作。
答案 3 :(得分:3)
编译器和x86 CPU将要小心移动与结构定义的字节数完全相同的字节数。没有x86指令可以在一次操作中移动24位,但是有8,16,32和64位数据的单指令移动。
如果向24位结构添加另一个字节字段(使其成为32位结构),您应该看到原子性返回。
某些编译器允许您在结构上定义填充,使其行为类似于本机寄存器大小的数据。如果填充24位结构,编译器将添加另一个字节,将大小“舍入”为32位,以便整个结构可以在一条原子指令中移动。缺点是你的结构总是占用内存空间的30%。
请注意,内存中结构的对齐对原子性也很重要。如果多字节结构不是在对齐的地址处开始,则它可能跨越CPU高速缓存中的多个高速缓存行。即使操作码是单个移动指令,读取或写入该数据也需要多个时钟周期和多个读/写。因此,如果数据未对齐,即使单个指令移动也可能不是原子的。 x86确保在对齐边界上进行原始大小读/写的原子性,即使在多核系统中也是如此。
使用x86 LOCK前缀可以通过多步移动实现内存原子性。但是应该避免这种情况,因为它在多核系统中可能非常昂贵(LOCK不仅阻止其他内核访问内存,它还会在操作期间锁定系统总线,这会影响磁盘I / O和视频操作.LOCK可能还强制其他核心清除他们的本地缓存)