std::atomic_uint64_t writing_ {0};
std::atomic_uint64_t reading_ {0};
std::array<type, size> storage_ {};
bool try_enqueue(type t) noexcept
{
const std::uint64_t writing {
writing_.load(std::memory_order::memory_order_relaxed)};
const auto last_read {reading_.load(std::memory_order::memory_order_relaxed)};
if (writing - last_read < size) {
storage_.at(writing & (size - 1)) = t;
writing_.store(writing + 1, std::memory_order::memory_order_release);
return true;
}
else
return false;
}
据我了解,在上面的代码中,如果条件的计算结果为 false,则任何线程都不可能观察到对共享存储的写入。操作不能被视为在之后排序的条件之前发生,这是否正确?还是我完全误读了这个,这样的事情实际上可能发生(也许是通过投机执行?
更具体地说,处理器是否可以推测性地执行写入(当条件最终评估为 false 时(,另一个线程观察到写入已经发生,然后第一个线程丢弃推测性写入?
(注意:这是单一生产者单一消费者(
不能被视为在条件之前发生,这是否正确?
C++编译器绝对不允许发明对atomic
(或volatile
(对象的写入。
编译器甚至不允许发明对非原子对象的写入(例如,将条件写入转换为读取 + cmov + 写入(,因为 C++11 引入了一个内存模型,该模型可以很好地定义两个线程同时运行这样的代码,只要最多其中一个线程实际写入(并且读取之后被排序(。 但是两个非原子 RMW 可能会相互踩踏,因此不能"像"C++抽象机器运行源代码一样"工作,因此编译器不能发出这样做的 asm。
但是,如果编译器知道一个对象总是被写入的,它几乎可以做任何它想做的事情,因为法律程序无法观察到差异:这将涉及数据竞争 UB。
更
具体一点,处理器是否可以推测性地执行写入(当条件最终评估为 false 时(,另一个线程观察到写入已经发生,然后第一个线程丢弃推测性写入?
不,投机并不能逃脱投机的核心。否则,当检测到错误推测时,所有内核都必须回滚其状态!
这是存储缓冲区存在的主要原因之一:将存储的 OoO 推理执行从提交分离到 L1d 缓存(这是当存储对其他核心全局可见时(。 并将执行与缓存未命中存储分离,这即使在有序的非推理 CPU 上也很有用。
在存储指令从无序核心(即已知是非推测的(停用之前,存储不会提交 L1d。 尚未提交的已停用存储有时称为"已毕业",以将它们与其他存储缓冲区条目区分开来,如果核心需要回滚到停用状态,则可能会丢弃这些条目。
这允许硬件推测执行,而无需发明写入。
(另请参阅推测执行的 CPU 分支是否可以包含访问 RAM 的操作码?了解更多详细信息。 有趣的事实:某些 CPU,特别是 PowerPC,可以在同一物理内核上的 SMT 线程之间对分级存储进行存储转发,使存储在某些内核全局可见之前对它们可见。但仅限于分级商店,否则可能会泄露出可能的误投。
在C++中,std::mo_release
存储强制编译器使用足够的屏障或发布存储指令(例如,x86 上的正常mov
是发布存储,或 AArch64 上的stlr
是顺序发布存储(。 或者任何其他机制来确保 asm 保证运行时排序至少与C++抽象机器保证的一样强大。
C++根据之前/之后的序列来定义其标准,而不是障碍,而是在任何给定的平台上实现/ABI标准化从std::atomic操作到ASM序列的某些映射。 (例如 https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html(