写时复制(Copy-on-Write)如何保障线程安全?C++正确实现探讨
线程安全的写时复制(Copy-on-Write)实现方案
你指出的原代码的两个线程安全问题完全正确:
- 检查
m_data && m_data.unique()的过程是非原子的,若其他线程在此时修改引用计数,会导致判断逻辑出错——比如明明当前不是唯一持有者,却误以为是,直接修改共享数据; rename中替换m_data和后续修改数据的操作之间没有同步,其他线程可能读到刚替换的空/默认数据,或者旧数据与新数据混合的异常状态。
要实现线程安全的COW,核心是确保所有对共享指针的读写操作都被同步保护,下面给出两种可行的实现方式:
方式一:使用互斥锁(兼容C++11及以上)
这是最直观且兼容性最好的方案,通过互斥锁确保同一时间只有一个线程能访问或修改共享指针:
#include <iostream> #include <string> #include <memory> #include <mutex> using namespace std; struct SPersonData { string m_name; string m_surname; }; class CPerson { private: mutable mutex m_mutex; // mutable允许const方法中加锁 shared_ptr<SPersonData> m_data; public: string getName() const; string getSurname() const; void rename(const string&, const string&); }; string CPerson::getName() const { lock_guard<mutex> lock(m_mutex); // 加锁,作用域结束自动释放 if (m_data) return m_data->m_name; return string(); } string CPerson::getSurname() const { lock_guard<mutex> lock(m_mutex); if (m_data) return m_data->m_surname; return string(); } void CPerson::rename(const string &name, const string &surname) { lock_guard<mutex> lock(m_mutex); // 检查是否需要复制:若指针为空或不是唯一持有者,创建新副本 if (!m_data || !m_data.unique()) { auto new_data = make_shared<SPersonData>(); // 可选:拷贝旧数据再修改,保留未改动字段(根据业务需求调整) if (m_data) { *new_data = *m_data; } m_data = move(new_data); } // 此时m_data是唯一持有者,安全修改数据 m_data->m_name = name; m_data->m_surname = surname; }
关键修改点:
- 新增
mutable mutex m_mutex:因为getName和getSurname是const方法,mutable允许在const方法中修改锁对象; - 所有访问
m_data的方法都用lock_guard加锁:确保读写操作互斥,彻底避免并发竞态; - 复制时可选拷贝旧数据:如果业务要求修改时保留其他未修改字段,可添加此逻辑(原代码未实现,可按需调整)。
方式二:使用原子化的shared_ptr(C++20及以上)
C++20引入了std::atomic<std::shared_ptr>,可以直接对shared_ptr进行原子操作,无需手动加锁:
#include <iostream> #include <string> #include <memory> #include <atomic> using namespace std; struct SPersonData { string m_name; string m_surname; }; class CPerson { private: atomic<shared_ptr<SPersonData>> m_data; public: string getName() const; string getSurname() const; void rename(const string&, const string&); }; string CPerson::getName() const { auto data = m_data.load(); // 原子加载当前指针 if (data) return data->m_name; return string(); } string CPerson::getSurname() const { auto data = m_data.load(); if (data) return data->m_surname; return string(); } void CPerson::rename(const string &name, const string &surname) { while (true) { auto current_data = m_data.load(); // 检查是否需要复制 if (current_data && current_data.unique()) { // 是唯一持有者,直接修改数据 current_data->m_name = name; current_data->m_surname = surname; break; } // 需要创建新副本 auto new_data = make_shared<SPersonData>(); if (current_data) { *new_data = *current_data; new_data->m_name = name; new_data->m_surname = surname; } else { new_data->m_name = name; new_data->m_surname = surname; } // 原子替换:只有当前指针和current_data一致时才替换,避免竞态 if (m_data.compare_exchange_weak(current_data, new_data)) { break; } // 替换失败说明其他线程修改了指针,重新循环尝试 } }
关键说明:
- 使用
std::atomic<shared_ptr<SPersonData>>存储共享指针,所有读写操作都是原子的; rename中用compare_exchange_weak实现无锁的原子替换:如果当前指针和加载的current_data一致,就替换成新指针,否则重新尝试,避免竞态;- 这种方式是无锁实现,性能比互斥锁更高,但逻辑更复杂,且仅支持C++20及以上版本。
核心原则总结
线程安全的COW必须满足:
- 所有对共享指针的访问(读/写)都必须同步:要么用互斥锁保护,要么用原子操作;
- 复制和替换指针的操作必须是原子的:不能出现判断和替换之间的间隙;
- 修改数据前必须确保是唯一持有者:只有当指针是唯一引用时,才能直接修改,否则必须创建副本。
内容的提问来源于stack exchange,提问作者just_code_dog
相关产品推荐
相关产品推荐

