比较并交换:通过不同的数据大小进行同步

15

使用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会像正常情况下一样失败吗?这种行为是否已经定义?在支持16b CAS的典型架构(如x86_64)上,这是否有可能“做正确的事情”?
编辑:为了明确起见,由于它似乎引起了混淆,我并不是在问上述行为是否由C++标准定义。显然,所有__atomic_*函数都是GCC扩展。 (然而,如果std::atomic_view成为标准化,未来的C++标准可能必须定义这种事情。)我更一般地询问了关于原子操作在典型现代硬件上的语义。例如,如果x86_64代码让2个线程对同一内存地址执行原子操作,但一个线程使用CMPXCHG8b,另一个使用CMPXCHG16b,以便一个对单词执行原子CAS,而另一个对双字执行原子CAS,这些操作的语义如何定义?更具体地说,CMPXCHG16b会因为观察到数据由于前面的CMPXCHG8b而发生变异而失败吗?
换句话说,两个使用不同数据大小(但相同的起始内存地址)的CAS操作能安全地用于在线程之间进行同步吗?

1
在相同的内存位置上,但使用较小的字长。听起来像是违反了严格别名规则 -> 未定义行为。 - too honest for this site
但是,对 &Foo 进行操作与对 &Foo.x 进行操作显然不违反别名规则,就像我们将 &Foo 传递给 memcpy 或将 &Foo.x 传递给 memcpy 一样。 - Siler
@Olaf 根据 C 和 C++ 标准,无论如何这种行为都是未定义的,因为它们没有说明 __atomic_compare_exchange 的语义。 - David Schwartz
1
@DavidSchwartz:在这种情况下,任何使用POSIX都会触发未定义的行为。 - 3442
@KemyLand 没错。使用 dlsym 获取函数地址是一个经典的例子。根据 C 标准,将 void * 强制转换为函数指针是未定义的。你必须让函数的语义超越语言。UB 参数仅适用于纯 C/C++ 代码或语义未由其他标准或语义规则指定的情况。 - David Schwartz
显示剩余6条评论
4个回答

5

其中一个操作会先发生,每个操作都按照自己的语义进行。

在x86 CPU上,这两个操作都需要锁定同一个缓存行,在整个操作期间保持锁定。因此,无论哪个操作首先获得该锁定,都不会看到第二个操作的任何影响,而获得第二个锁定的操作将看到第一个操作的所有影响。两个操作的语义将被完全尊重。

其他硬件可能以其他方式实现此结果,但如果它没有实现此结果,则除非指定了限制,否则它就是有问题的。


4
原子数据最终将位于内存中的某个位置,所有对它的访问(或对应缓存的访问,当操作是原子的时候)都将被串行化。由于CAS操作应该是原子的,它将整体执行或者完全不执行。
话虽如此,其中一个操作将成功,第二个将失败。顺序是不确定的。
来自x86 Instruction Set Reference
这条指令可以用LOCK前缀来执行原子操作。为了简化处理器总线的接口,目标操作数在比较的结果不考虑的情况下接收写周期。如果比较失败,则将目标操作数写回;否则,源操作数将写入目标操作数。(处理器永远不会产生只有锁定读而没有锁定写的情况。)
显然,两个线程都将尝试在锁定读之后进行锁定写(当与LOCK前缀一起使用时),这意味着它们中只有一个将成功地执行CAS,另一个将读取已经更改的值。

1

在检查潜在冲突的原子操作时,硬件通常非常保守。甚至可能发生两个针对完全不重叠的地址范围的CAS操作被检测到相互冲突的情况。


1

“原子性”的定义不太可能改变,重点在于保证并发进程的隔离。

在并发编程中,如果一个操作(或一组操作)是“原子的”、“可线性化的”、“不可分割的”或“不可中断的”,那么对于系统的其余部分来说,它似乎是瞬间发生的。原子性是保证与并发进程隔离的保证

您的问题是...

如果对较大的数据大小执行原子CAS操作时,观察到同一内存位置上使用较小数据大小的原子操作发生了更改,会发生什么?

按照定义,不能同时改变使用原子操作修改的两个重叠的内存区域,即这两个操作必须线性进行,否则它们就不是原子操作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接