使用GCC内置C原子基元,我们可以使用__atomic_compare_exchange
执行原子CAS操作。
与C ++ 11的std::atomic
类型不同,GCC C原子基元在常规非原子积分类型上运行,包括支持cmpxchg16b
的平台上的128位整数。 (C ++标准的未来版本可能支持与std::atomic_view
类模板类似的功能。)
这让我有疑问:
如果对较大数据大小的原子CAS操作观察到同一内存位置上的原子操作发生的更改,但使用较小的数据大小会发生什么?
例如,假设我们有:
struct uint128_type {
uint64_t x;
uint64_t y;
} __attribute__ ((aligned (16)));
假设我们有一个uint128_type
类型的共享变量,例如:
uint128_type Foo;
现在,假设线程A执行:
Foo expected = { 0, 0 };
Foo desired = { 100, 100 };
int result = __atomic_compare_exchange(
&Foo,
&expected,
&desired,
0,
__ATOMIC_SEQ_CST
);
线程B确实:
uint64_t expected = 0;
uint64_t desired = 500;
int result = __atomic_compare_exchange(
&Foo.x,
&expected,
&desired,
0,
__ATOMIC_SEQ_CST
);
如果线程A的16字节CAS发生在线程B的8字节CAS之前(或反之亦然),会发生什么? CAS是否正常失败?这甚至是定义的行为吗?这有可能"做正确的事情"在像x86_64这样支持16b CAS的典型架构上?
编辑:要清楚,因为它似乎引起了混淆,我不询问上述行为是否由C ++标准定义。显然,所有__atomic_
*函数都是GCC扩展。 (但是,如果std::atomic_view
变得标准化,未来的C ++标准可能必须定义这种东西。)我更普遍地询问典型现代硬件上原子操作的语义。例如,如果x86_64代码有2个线程对同一个内存地址执行原子操作,但是一个线程使用CMPXCHG8b
而另一个线程使用CMPXCHG16b
,那么一个线程对单个字执行原子CAS,而另一个是双字的原子CAS,这些操作的语义是如何定义的?更具体地说,CMPXCHG16b
是否会失败,因为它会观察到由于之前的CMPXCHG8b
而数据已从预期值发生变异?
换句话说,使用两种不同数据大小(但相同的起始内存地址)的两种不同CAS操作可以安全地用于线程之间的同步吗?
答案 0 :(得分:4)
首先发生一个或另一个,每个操作都按照自己的语义进行。
在x86 CPU上,两个操作都需要锁定整个操作中保持的同一缓存行。因此,无论哪一个获得该锁定,都不会看到第二个操作的任何影响,无论哪个获得该锁定,都会看到第一个操作的所有效果。两个操作的语义都将得到充分尊重。
其他硬件可能会以其他方式实现此结果,但如果它没有达到此结果,除非它指定它有限制,否则它会被破坏。
答案 1 :(得分:3)
最终,原子数据将位于内存中的某个位置,并且所有对它的访问(或者当操作是原子的时,对各个缓存的访问)都将被序列化。由于CAS操作应该是原子的,它将作为一个整体执行或根本不执行。
话虽这么说,其中一项行动将取得成功,第二项行动将失败。订单是不确定的。
来自x86 Instruction Set Reference:
该指令可与LOCK前缀一起使用,以允许指令以原子方式执行。为了简化处理器总线的接口,目标操作数接收写周期而不考虑比较结果。如果比较失败,则写回目标操作数;否则,源操作数将写入目标。 (处理器永远不会产生锁定读取,也不会产生锁定写入。)
显然,两个线程都会在锁定读取后尝试锁定写入(当与LOCK前缀一起使用时),这意味着只有其中一个将成功执行CAS,另一个将读取已经更改的值。 / p>
答案 2 :(得分:1)
原子的定义不太可能改变,强调我的
在并发编程中,一个操作(或一组操作)是原子的, 如果它可以线性化,不可分割或不可中断 系统的其余部分即时发生。原子性是一种 保证隔离来自并发进程。
你的问题是......
如果观察到更大数据大小的原子CAS操作会发生什么 由同一记忆中的原子操作发生的变化 位置,但使用较小的数据?
根据定义,使用原子操作修改的两个重叠的内存区域不能同时发生变异,即两个操作必须线性发生或不发生原子。
答案 3 :(得分:1)
在检查潜在冲突的原子操作之间的冲突时,硬件通常非常保守。甚至可能发生两个CAS操作到两个完全不同的,非重叠的地址范围可能被检测到彼此冲突。