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

ger*_*rdi 4 c++ concurrency lock-free race-condition stdatomic

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

\n
//somewhere else writePosition.store(...,release)\n\nbool pop(size_t & returnValue){\nwritePosition = writePosition.load(aquire)\noldReadPosition = readPosition.load(relaxed)\nsize_t value{};\ndo{\n  value = data[oldReadPosition]\n  newReadPosition = oldReadPosition+1\n}while(readPosition.compare_exchange(oldReadPosition, newReadPosition, relaxed)\n// here we are owner of the value\nreturnValue = value;\nreturn true;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

data[oldReadPosition]仅当该值之前从另一个线程读取时,才能更改内存。

\n

读写位置都是 ABA 安全的。\n通过简单的复制,value = data[oldReadPosition]内存data[oldReadPosition]不会被改变。

\n

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

\n

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

\n

多谢

\n

Pet*_*des 5

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

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

非原子读取然后检查可能的撕裂的另一个用例是 SeqLock,其中读者可以通过在非原子读取之前和之后从原子计数器读取相同的值来证明没有撕裂。即使对于非原子数据,它也是 C++ 中的 UB volatile,尽管这可能有助于确保编译器生成的 asm 是安全的。(利用内存屏障和现有编译器对原子的当前处理,即使是非易失性也可以使汇编工作)。请参阅在固定不同 CPU 的 2 个线程之间传递一些变量的最佳方式

atomic_thread_fence对于 SeqLock 的安全性和一些必要的原子加载顺序仍然是必要的。如果非原子无法与某些内容同步并创建发生之前,则它可能是一个实现细节。

人们确实在现实生活中使用 Seq Locks,这取决于现实生活中的编译器实际上定义的行为比 ISO C++ 多一点。或者换句话说,现在碰巧有效;如果您对非原子读取周围放置的代码非常小心,那么编译器不太可能做任何有问题的事情。

但是您肯定会冒险越过保证行为的安全区域,并且可能需要了解 C++ 如何编译为 asm,以及 asm 如何在您关心的目标平台上工作;另请参阅谁害怕一个糟糕的优化编译器?在 LWN 上;它针对的是 Linux 内核代码,它是手卷原子和类似东西的主要用户。