线程安全的复制需满足:只读共享、独占转移或封装同步;否则并发拷贝同一对象必然出错,因拷贝非原子且无自动同步。

多线程中“复制对象”本身不是原子操作,安全与否不取决于“怎么复制”,而取决于“谁在访问、是否共享、有无同步”。直接调用拷贝构造函数或 operator= 在多个线程间并发操作同一对象,必然出问题。
拷贝构造函数和赋值运算符本身不线程安全
即使你实现了深拷贝、甚至把 operator= 声明为 noexcept,只要两个线程同时对**同一个源对象**执行拷贝(比如 auto a = obj; 和 auto b = obj;),就可能读到中间状态——尤其是当 obj 正在被另一个线程修改时。
- 编译器生成的默认拷贝构造函数是逐成员复制,对指针成员只复制地址(浅拷贝),若源对象内部有裸指针且正被修改,拷贝结果不可预测
- 手写深拷贝构造函数虽能隔离数据,但前提是源对象在拷贝期间**不被其他线程修改**;否则仍会读到撕裂值(如部分字段已更新、部分未更新)
- 拷贝过程不自动加锁,也不触发内存屏障;C++ 标准不保证任何并发读写的顺序一致性
shared_ptr 管理的对象仍需额外保护
std::shared_ptr 的引用计数增减是线程安全的,但这只意味着你能安全地拷贝/销毁 shared_ptr 实例本身。它完全不管其指向的对象。
- 错误写法:
sp->data++在多个线程中无锁调用 → 非原子读-改-写,结果未定义 - 正确做法:对
*sp的访问必须加std::mutex,或把内部字段改为std::atomic<t></t> - 传
shared_ptr给新线程是安全的(引用计数+1),但一旦线程开始访问sp.get()或解引用,就必须确认该对象的生命周期和数据访问同步策略
真正安全的复制场景只有三种
所谓“安全复制”,本质是规避竞态:要么隔离访问,要么控制所有权,要么序列化操作。
-
只读共享 + 复制值:源对象在所有线程中只读(
const且无内部可变状态),此时任意线程调用拷贝构造函数都是安全的 —— 因为没有写入,无需同步 -
独占转移 + move 替代 copy:用
std::move把对象从一个线程移入队列(如queue.push(std::move(obj))),再由另一线程移出使用。前提是队列本身线程安全(如std::queue+std::mutex,或moodycamel::ConcurrentQueue) -
封装同步逻辑到类接口中:在类内部提供带锁的
clone()方法,例如std::shared_ptr<myclass> clone() const { std::lock_guard l(mtx_); return std::make_shared<myclass>(*this); }</myclass></myclass>;调用者无需关心锁,但性能开销明确
容易被忽略的关键点
很多人以为“用了 shared_ptr 就能随便复制”,或者“深拷贝函数写对了就万事大吉”,其实最危险的恰恰是那些没被显式保护的隐式共享:
- 拷贝构造函数里调用了某个非线程安全的全局函数或静态变量
- 对象内部持有
std::weak_ptr,而lock()后没检查返回值是否为空,直接解引用 - 自定义类中重载了
operator=,但忘了把关键字段的赋值操作放在锁内,导致部分字段更新、部分未更新 - 误把
std::is_nothrow_copy_constructible_v<t></t>当作线程安全依据 —— 它只说明不抛异常,和并发无关
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











