关于memory_order内存序配对与环形缓冲区线程安全的技术咨询
背景
我正在为解释器开发一款采样器,要求解释器每次调用时将当前帧位置写入指定位置,之后每隔X毫秒采样该信息。最初使用rigtorp自旋锁保护帧位置,但性能分析显示解释器每次循环的锁获取时间占比很高,对运行时性能影响较大。查阅内存屏障相关资料后,我设计了一个更高效的方案,现需确认对memory_order_relaxed与acquire/release关系的理解是否正确。
实现代码
#include <memory> #include <chrono> #include <string> #include <iostream> #include <thread> #include <immintrin.h> #include <cstring> #include <atomic> using namespace std; typedef struct frame { uint8_t op; uint16_t arg; uint32_t check; } frame; static constexpr unsigned int FRAME_BUFFER_SIZE = 8 * 1024; static atomic<unsigned int> index; static frame frames[FRAME_BUFFER_SIZE]; static void writer() { uint8_t op = 0; uint16_t arg = 0; for (;;) { op++; arg++; const auto newIndex = index.load(memory_order_relaxed) + 1; auto &target = frames[newIndex % FRAME_BUFFER_SIZE]; target.op = op; target.arg = arg; target.check = static_cast<uint32_t>(arg) + op; index.store(newIndex, memory_order_release); _mm_pause(); // 给其他线程让出一些时间 } } static void reader() { for (;;) { const auto lastValidIndex = index.load(memory_order_acquire); // 我们存在竞态,但假设环形缓冲区足够大,避免写入线程追上我们 const auto snapshot = frames[lastValidIndex % FRAME_BUFFER_SIZE]; if ((static_cast<uint32_t>(snapshot.arg) + snapshot.op) != snapshot.check) { cout << "无效的快照\n"; exit(1); } // 休眠一段时间,因为采样器只需要偶尔读取一次 this_thread::sleep_for(chrono::milliseconds(1)); } } int main() { cout << "开始运行\n"; index = 0; memset(frames, 0, sizeof(frames)); thread w(writer); thread r(reader); w.join(); r.join(); return 0; }
设计策略
采用环形缓冲区,写入线程是唯一修改index变量的线程:
- 写入线程读取
index时使用memory_order_relaxed; - 更新数组中的帧数据后,用
memory_order_release存储新的“完整”index; - 读取线程仅使用
memory_order_acquire读取index,再访问数组对应位置。
问题与解答
1. 这种内存屏障的配对是否能保证对frames数组的写入操作早于index的更新?
可以保证。memory_order_release会确保该存储操作之前的所有非原子内存写入(即对frames数组的修改)都完成并对其他线程可见,不会被重排到index.store(..., memory_order_release)之后。读取线程的memory_order_acquire会确保后续对frames的读取操作不会被重排到index.load(memory_order_acquire)之前,并且能看到写入线程在release存储之前的所有写入操作。两者配对形成了happens-before关系:写入线程对frames的修改 happens before index的release存储,而index的acquire读取 happens before 读取线程对frames的读取,因此整个链路的可见性和顺序都得到了保证。
2. 每次执行memory_order_acquire时,读取线程的CPU缓存是否会被清空?
不会。memory_order_acquire的作用不是清空缓存,而是限制内存操作的重排序,并确保当前线程能看到其他线程通过release操作发布的内存修改。具体到硬件层面,它通常会阻止后续的加载/存储操作被重排到acquire加载之前,可能会通过栅栏指令(如x86上的lfence,但x86本身对加载的重排有严格限制,acquire在x86上几乎等同于普通加载)来实现,但不会清空整个缓存。缓存的管理由CPU自动进行,acquire只是建立内存可见性约束,不会直接操作缓存内容。
3. 由于写入线程是唯一修改index变量的线程,且我们不关心frames数组中的旧值,使用memory_order_relaxed读取index是否安全?
安全。因为写入线程是唯一修改index的线程,不存在多线程同时修改index的情况,所以写入线程读取index时用memory_order_relaxed不会有数据竞争问题。relaxed语义保证了该读取操作是原子的,不会读到部分更新的值,而我们只需要基于当前的index值计算下一个位置,不需要和其他线程的操作建立顺序关系,因此完全可以使用memory_order_relaxed来提升性能。
内容的提问来源于stack exchange,提问作者Davy Landman

