C++11实现读写者锁是否需额外添加内存屏障?
问题背景
近期需要使用C++11及后续版本引入的线程、同步原语实现读者-写者问题,找到的参考实现代码如下:
读线程逻辑
// --- read code rw_mtx.lock(); // will block if there is a write in progress read_count += 1; // announce intention to read rw_mtx.unlock(); cell_value = data_base[cell_number]; rw_mtx.lock(); read_count -= 1; // announce intention to read if (read_count == 0) rw_write_q.notify_one(); rw_mtx.unlock();
写线程逻辑
// --- write code std::unique_lock<std::mutex> rw_lock(rw_mtx); write_count += 1; rw_write_q.wait(rw_lock, []{return read_count == 0;}); data_base[cell_number] = cell_value; write_count -= 1; if (write_count > 0) rw_write_q.notify_one();
核心疑问
针对上述实现代码,存在三点疑问:
- 写线程执行
data_base[cell_number]写入操作前,是否需要添加内存屏障(memory-barrier/fence)实现共享内存的访问同步? - 读线程读取共享数据前是否也需要做相同的内存屏障处理?
- 如果确实需要添加内存屏障,具体应当如何实现?
回答
不需要额外手动添加内存屏障,你用到的C++标准库线程同步原语本身已经自带完备的内存序保证,完全可以覆盖共享数据访问的同步需求,具体逻辑如下:
- 首先明确基础规则:
std::mutex的lock/unlock操作天然自带全序内存屏障效果,严格遵循先行发生(happens-before)原则:同一个互斥量上,前一个线程的unlock操作,一定和后续其他线程拿到锁的lock操作形成同步关系——unlock之前所有对共享内存的写入,在lock返回后对当前持锁线程完全可见;同时编译器、CPU都不能把临界区内的内存访问操作重排到临界区外,从根本上避免了指令重排导致的可见性问题。- 对写线程:
rw_write_q.wait()返回的前提是已经成功持有rw_mtx,这时候所有之前持有过这把锁的线程(最后一个退出的读线程、之前写完释放锁的写线程)做的所有内存修改都已经对当前写线程可见,直接执行data_base[cell_number] = cell_value写入不会有可见性问题。这次写入本身在持锁临界区内,等写操作完成、锁释放的时候,写入的结果会自动对后续拿锁的线程可见,根本不需要额外加屏障。 - 对读线程:开头先拿
rw_mtx、给read_count加1再放锁的操作,已经和写线程的加锁操作形成了同步——如果这时候能成功拿到锁、把读者计数加1,就说明当前没有活跃写者,之前所有写操作的结果都已经同步完成,直接读取data_base[cell_number]不会读到半写的脏数据。读完之后再拿锁修改读者计数、唤醒等待写者的操作,同样会通过互斥量的同步规则,和后续写线程的加锁操作形成配对,保证读写不会出现数据竞争。
- 对写线程:
- 其次,代码中用到的
std::condition_variable的wait、notify_one接口本身也适配了互斥量的内存序:wait阻塞结束返回前一定会重新持有互斥量,相当于自带了一次lock的内存同步效果;notify_one只是唤醒等待队列上的线程,不会破坏互斥量已经建立的同步关系,也不会引入数据竞争。
补充说明:只有完全不使用互斥量、靠
std::atomic原子变量做无锁同步的时候,才需要手动指定内存序、必要时添加独立内存屏障。只要是用标准库封装好的互斥量、条件变量、std::unique_lock这类RAII锁包装器,就完全不需要自己额外加fence——手动乱加屏障不仅会带来无意义的性能开销,放错位置还会引发极难排查的同步bug。
顺带提一句,你贴的这份实现是写者优先的读者-写者方案,会阻塞后续新读者进入直到等待的写者完成操作,不会出现写者饥饿,但可能出现读者饥饿,这是同步策略选择问题,和内存同步逻辑无关。
内容的提问来源于stack exchange,提问作者nsm_raw
相关产品推荐
相关产品推荐

