在两个线程上读/写 64 位,无互斥/锁定/原子

Read/write 64-bits on two threads without mutex/lock/atomics

本文关键字:原子 锁定 两个 线程      更新时间:2023-10-16

我有一个 64 位结构std::array,其中每个结构如下所示:

MyStruct
{
float _a{0};
float _b{0};
};  // Assume packed

一个线程(CPU 内核(将写入 64 位对象,另一个线程(不同的内核(将读取它。

我使用的是英特尔 x86 架构,我知道 64 位写入保证是原子的,来自英特尔开发人员手册。

但是,我担心第二个线程可能会在寄存器中缓存该值,并且无法检测到该值何时更改。

  • MESIF协议会保证第二个线程看到写入吗?
  • 我是否需要volatile关键字来告诉编译器另一个线程可能正在修改内存?
  • 我需要原子学吗?

写入值的线程对性能非常敏感,如果可以的话,我想避免内存障碍、互斥锁等。

无论volatile是否会在下一个C++版本中被弃用 -volatile从未设计或打算用于多线程!这与Java形成鲜明对比,Java的易失性意味着完全不同的东西(Java易失性语义更接近于C++原子学的语义(。

最好有更多关于实际问题的信息,即,关于你实际想要实现的目标的更多背景。

根据您的描述,您只涉及两个线程 - 一个读取和一个写入 - 我建议使用单生产者-单消费者队列。这样的队列可以只用两个用于头/尾索引的原子计数器来实现;值本身不必是原子的,可以是任何类型的(包括不可复制的值(。

但是要了解这是否是一个有效的解决方案,我需要更多信息: 这些物品应该以先进先出还是后进先出消费?阵列呢?它有多大?它可以溢出/下溢(即线程尝试写入/读取条目,但数组已满/空(?应如何处理已满/空数组?

作为一个C++开发人员,你应该对 cpu 的低级功能持保留态度。请参阅这个有趣的问题:"理解标准::hardware_destructive_interference_size和标准::hardware_constructive_interference_size">

在缓存行之间真正共享appens,从我们可以看到的,上面的结构应该像这样修改:

struct MyStruct
{
alignas ( hardware_constructive_interference_size ) atomic < float > _a;
alignas ( hardware_constructive_interference_size ) atomic < float > _b;
}; 

并发访问变量总是需要使用 std::atomic。如果您的目标应用程序要按顺序执行写入对您来说并不重要C++。 很多事情都在引擎盖下进行,最后易失性不起作用,它已被std::atomic超级种子并被弃用。

MESIF协议会保证第二个线程看到写入吗?

不,这取决于操作系统,如果你的第一个(写入线程(被优先考虑,并且在第二个线程甚至可以读取第一个线程之前设法写入两次,那么这就是数据竞争,完全依赖于操作系统。

我是否需要 volatile 关键字来告诉编译器另一个线程可能正在修改内存?

volatile告诉编译器不要优化变量,根本不优化它。

我需要原子学吗?

取决于,您不打算使用互斥锁,您不打算使用任何与远程并发相关的内容,除了原子学,它更适用于跨线程编写

我建议将 std::mutex 与std::lock_guardstd::scoped_lock结合使用。

我知道你的标题说你不想要它,但这确实是保证每次阅读和写作顺序相同的唯一方法。

  • MESIF - 对寄存器没有影响。
  • 易失性 - 在过去用于此,但在较新的编译器中,它仅用于从硬件寄存器读取以强制读取,因此它不起作用。
  • 原子 - 一旦你有两个线程一起通信,你就需要原子操作来读取和写入。

    std::atomic whatEver;

如果你幸运的话,你会得到

bool is_lock_free = whatEver.is_lock_free();

更幸运的是,如果

bool is_lock_free = whatEver.is_always_lock_free();

你将不得不做

auto whatEver = arrayOfWhatEver[x]; // atomic
auto a = whatEver._a;

使用原子操作,而不仅仅是读取 MyStruct 的各个成员。