是在不使用未定义行为的情况下对基本类型的非原子变量进行并发写入和读取吗?

is a concurent write and read to a non-atomic variable of fundamental type without using it undefined behavior?

在无锁 queue.pop() 中,在与循环内的原子获取同步后,我读取了一个 trivialy_copyable 变量(整型)。 最小化的伪代码:

//somewhere else writePosition.store(...,release)

bool pop(size_t & returnValue){
writePosition = writePosition.load(aquire)
oldReadPosition = readPosition.load(relaxed)
size_t value{};
do{
  value = data[oldReadPosition]
  newReadPosition = oldReadPosition+1
}while(readPosition.compare_exchange(oldReadPosition, newReadPosition, relaxed)
// here we are owner of the value
returnValue = value;
return true;
}

data[oldReadPosition] 的内存只能更改,前提是这个值是从另一个线程读取的。

读写头寸是 ABA 安全的。 通过简单的复制,value = data[oldReadPosition]不会改变data[oldReadPosition]的内存。

但是写入线程 queue.push(...) 可以在读取时更改 data[oldReadPosition],当且仅当另一个线程已经读取了 oldPosition 并更改了 readPosition。

如果您使用该值,这将是一个竞争条件,但当我们保持 value 不变时,它是否也是一个竞争条件,因此是未定义的行为?该标准不够具体,或者我不明白。 imo,这应该是可能的,因为它没有效果。 我会很高兴得到一个合格的答案以获得更深入的见解

非常感谢

是的,它是 ISO C++ 中的 UB; value = data[oldReadPosition] 在 C++ 抽象机中涉及读取该对象的值。 (通常这意味着左值到右值的转换,IIRC。)

但它基本上是无害的,可能只会在具有硬件竞争检测的机器上出现问题(不是普通的主流 CPU,但可能在 C 实现上,如带有 threadansanitizer 的 clang)。

另一个 use-case for non-atomic 读取然后检查可能的撕裂是 SeqLock,读者可以通过从中读取相同的值来证明没有撕裂non-atomic 读取前后的原子计数器。它是 C++ 中的 UB,即使 volatile 用于 non-atomic 数据,尽管这可能有助于确保 compiler-generated asm 是安全的。 (由于存在内存障碍和现有编译器当前对原子的处理,即使 non-volatile 也可以工作 asm)。参见

atomic_thread_fence 仍然是 SeqLock 安全所必需的,并且原子加载的一些必要顺序 wrt。 non-atomic 可能是一个实现细节,如果它不能与某些东西同步并创建一个 happens-before。

人们确实在现实生活中使用序列锁,这取决于 real-life 编译器 de-facto 定义的行为比 ISO C++ 多一点。或者换句话说,它恰好现在有效;如果您对放在 non-atomic 周围的代码很小心,那么编译器不太可能做任何有问题的事情。

但是您肯定会冒险越过保证行为的安全区域,并且可能需要了解 C++ 如何编译为 asm,以及 asm 如何在您关心的目标平台上工作;另见 LWN 上的 Who's afraid of a big bad optimizing compiler?;它针对 Linux 内核代码,它是 hand-rolled 原子和类似东西的主要用户。