多线程写入内存映射文件:刷盘与解映射前需同步吗?
问题解答
1. 是否需要同步写入线程与刷盘线程?
是的,你需要正确的内存同步机制,仅用relaxed原子操作和编译器屏障不足以保证正确性:
- 写入线程:
NUM_WRITERS.fetch_sub(1)必须使用memory_order_release而非relaxed。release语义会确保memcpy的所有写入操作,在NUM_WRITERS的递减操作对其他线程可见前就已完成,且对后续同步操作可见(包括CPU缓存层面的同步)。单纯的编译器屏障只能阻止编译器指令重排,无法约束CPU的指令重排行为。 - 刷盘等待线程:循环中加载
NUM_WRITERS时必须使用memory_order_acquire而非relaxed。acquire语义会确保执行flush_async和unmap前,所有写入线程通过release语义发布的写入操作都已被当前线程可见。
2. CPU和操作系统是否保证缓存写入最终到磁盘?
- CPU的缓存一致性机制仅保证线程间的缓存数据同步,但不会自动将缓存数据刷写到磁盘。操作系统虽会定期异步刷新页缓存到磁盘,但这个过程是不确定的,无法依赖它来保证数据及时持久化。
- 你必须显式调用
msync(Linux)或FlushViewOfFile(Windows)来强制数据从主内存写入磁盘。在此之前,需要通过内存屏障确保所有写入线程的数据已从CPU缓存同步到主内存(或至少对操作系统的刷盘操作可见)。
3. 能否仅使用relaxed原子操作追踪数据?
CURRENT_OFFSET使用relaxed是安全的:因为每个线程的写入区域不重叠,原子操作的原子性(与内存顺序无关)已能保证每个线程获得唯一偏移量,不会出现冲突。- 但
NUM_WRITERS的操作不能全用relaxed:如前所述,必须通过release/acquire语义建立写入操作与刷盘操作之间的happens-before关系,确保刷盘时所有写入都已完成且可见。
修正后的伪代码
static AtomicUint NUM_WRITERS = 0; static AtomicUint CURRENT_OFFSET = 0; void some_thread_fn(void* buffer, const uint8_t* payload, size_t payload_size, size_t buffer_size) { NUM_WRITERS.fetch_add(1, memory_order_relaxed); size_t offset = CURRENT_OFFSET.fetch_add(payload_size, memory_order_relaxed); void* dst = (uint8_t*)buffer + offset; memcpy(dst, payload, payload_size); // 使用release语义确保memcpy写入在NUM_WRITERS递减前完成 NUM_WRITERS.fetch_sub(1, memory_order_release); if (offset + payload_size < buffer_size) { // 由其他线程负责解映射 return; } // 使用acquire语义等待所有写入完成,并确保所有写入可见 while (NUM_WRITERS.load(memory_order_acquire) > 0) { _mm_pause(); } flush_async(buffer); unmap(buffer); }
关键修正点:
- 将
NUM_WRITERS.fetch_sub(1)的内存顺序改为memory_order_release - 将循环中
NUM_WRITERS.load()的内存顺序改为memory_order_acquire - 移除不必要的编译器屏障,因为原子操作的memory_order已包含编译器和CPU的重排约束
内容的提问来源于stack exchange,提问作者Angelicos Phosphoros
相关产品推荐
相关产品推荐

