std::unique_ptr、std::shared_ptr、std::weak_ptr 全部都是「部分线程安全」 ,没有任何一个是完全线程安全的,也没有任何一个是「完全不安全」的;
三者的线程安全特性差异极大 ,核心根源是:设计初衷不同、内存管理方式不同、是否包含引用计数、是否独占资源。
补充:所有 C++ 标准库智能指针的线程安全规则,是C++ 标准强制规定的,GCC/Clang/MSVC 所有编译器实现完全一致,无版本差异。
根本原则
所有智能指针,只保证「智能指针对象自身」的线程安全边界 ,永远不保证「其指向的堆内存资源」的线程安全。
智能指针不是「线程安全的银弹」:多个线程通过任意智能指针操作同一份堆内存资源时,资源的读写安全,完全由开发者自己保证(加锁),和用什么智能指针无关,和裸指针的规则完全一致。
- 多线程只读 同一份堆资源 → 线程安全,无需加锁;
- 多线程读写 / 写写 同一份堆资源 → 线程不安全,必须加锁 (
std::mutex),否则数据竞争、内存错乱、程序崩溃;
一、std::unique_ptr 线程安全特性(独占型智能指针)
核心特性
- 独占所有权 :一个
unique_ptr独占一份堆资源,同一时间只能有一个unique_ptr指向该资源 ,不允许拷贝,只能移动(std::move); - 无引用计数 :
unique_ptr是轻量级智能指针,内部只有一个裸指针,没有任何引用计数,也不会在堆上开辟额外内存; - 极致高效:所有操作都是编译期的指针拷贝 / 移动,无运行时开销。
规则 1:多线程「只读」同一个 unique_ptr 对象 → 线程安全
多个线程对同一个 unique_ptr 只执行读操作 ,不会有任何问题,无需加锁。只读操作包含 :get()获取裸指针、if(up)判空、*up解引用、up->成员访问、release()(注意:release 是释放所有权,不算写操作)。
cpp
std::unique_ptr<int> up = std::make_unique<int>(10);
// 线程1:只读
void read1() { if(up) cout << *up << endl; }
// 线程2:只读
void read2() { cout << up.get() << endl; }
规则 2:多线程「写 / 读写混合」同一个 unique_ptr 对象 → 极度不安全(风险最高)
这是 unique_ptr 最核心的不安全场景,风险比 shared_ptr 更高 ,一旦出现必出问题,且问题更隐蔽、更难排查。原因:unique_ptr 无引用计数,所有写操作都是直接修改内部的裸指针成员,这个操作是「非原子的」,且无任何保护机制。
写操作包含 :reset()重置指针、up = nullptr赋值空、std::move(up)移动所有权、up = std::make_unique<int>(20)赋值新资源、析构持有资源的unique_ptr。
cpp
std::unique_ptr<int> up = std::make_unique<int>(10);
// 线程1:写操作 → 移动所有权,原up变为空
void write1() { auto up1 = std::move(up); }
// 线程2:写操作 → 重置指针
void write2() { up.reset(); }
// 线程3:读+线程1/2写 → 读写混合,大概率野指针崩溃
void read3() { cout << *up << endl; }
重点:
unique_ptr的「独占性」≠「线程安全性」,独占只是语法层面禁止拷贝,无法保证多线程的并发修改安全。
使用特点
在实际开发中,极少遇到「多线程操作同一个 unique_ptr」的场景,因为它的设计初衷就是「独占」,一般是:
- 单个线程内创建、使用、销毁
unique_ptr; - 通过
std::move将所有权「转移」给其他线程,转移后原线程的unique_ptr就为空了,不会再操作。→ 这一特点让unique_ptr在工程中几乎不会出现线程安全问题。
二、std::shared_ptr 线程安全特性(共享型智能指针)
核心特性
- 共享所有权 :多个
shared_ptr可以指向同一份堆资源,互相不冲突; - 双引用计数 :堆上开辟独立的「计数块」,包含 强引用计数 (use_count) 和 弱引用计数 (weak_count) ,所有计数的增减都是原子操作(CPU 原子指令实现,无锁);
- 有运行时开销 :拷贝 / 析构时需要原子操作增减计数,比
unique_ptr稍重,但开销极小。
规则 1:「引用计数的增减」绝对线程安全(标准强制保证,无任何例外)
多个线程操作不同的 shared_ptr 对象 ,但这些对象指向同一份资源 时,对「强 / 弱引用计数」的自增 / 自减,是原子操作 ,完全线程安全,无需加锁。这是 shared_ptr 最核心的线程安全特性,也是它能「安全共享」的根基。
cpp
std::shared_ptr<int> sp = std::make_shared<int>(10);
// 线程1:拷贝sp,强引用计数+1(原子安全)
void func1() { std::shared_ptr<int> sp1 = sp; }
// 线程2:析构sp2,强引用计数-1(原子安全)
void func2() { std::shared_ptr<int> sp2 = sp; sp2.reset(); }
规则 2:多线程「只读」同一个 shared_ptr 对象 → 线程安全
和 unique_ptr 一致,只读同一个 shared_ptr 不会有任何问题,所有查询类操作都是安全的。
规则 3:多线程「写 / 读写混合」同一个 shared_ptr 对象 → 线程不安全
对同一个 shared_ptr 的写操作(赋值、reset、move、析构),本质是「修改内部裸指针 + 增减引用计数」的组合操作,这个组合操作是「非原子的」。
比如:线程 1 执行sp.reset(),刚把计数 - 1,还没把内部指针置空;线程 2 此时执行*sp,就会访问野指针,程序崩溃。
解决方案 :对同一个 shared_ptr 的所有写操作,加 std::mutex 互斥锁即可。
三、std::weak_ptr 线程安全特性(弱引用智能指针,依附 shared_ptr 存在)
核心特性
- 无所有权 :
weak_ptr是shared_ptr的「附属品」,不持有资源的强引用,不会影响资源的生命周期,也不能直接解引用访问资源; - 依附共享计数 :
weak_ptr绑定到shared_ptr的「双引用计数块」,自身包含弱引用计数,增减也是原子操作; - 核心作用 :解决
shared_ptr的循环引用内存泄漏 问题,通过lock()方法「升级」为shared_ptr后才能访问资源。
std::weak_ptr的线程安全特性,和std::shared_ptr完全一致、100% 继承,无任何额外的线程安全规则,也无任何额外风险。
规则 1:「弱引用计数的增减」绝对线程安全(原子操作)
多个线程操作不同的 weak_ptr 对象,但绑定同一份资源时,弱引用计数的自增 / 自减是原子操作,安全无锁。
规则 2:多线程「只读」同一个 weak_ptr 对象 → 线程安全
只读操作:wp.expired()判资源是否存活、wp.use_count()获取强引用计数、wp.lock()(只读场景下),均安全。
规则 3:多线程「写 / 读写混合」同一个 weak_ptr 对象 → 线程不安全
写操作包含:wp = sp赋值、wp.reset()重置、wp = std::move(wp1)移动,这些操作会修改 weak_ptr 内部的指针,非原子操作,存在数据竞争,需要加锁保护。
weak_ptr 额外安全点
weak_ptr::lock()方法:是原子安全 的,调用时会原子性的检查强引用计数 + 生成新的shared_ptr,不会出现中间态;- 即使
weak_ptr操作出错,最多返回空的shared_ptr,不会导致资源泄漏、野指针、程序崩溃,风险远低于另外两个指针;
四大误区
误区 1:unique_ptr 是独占的,所以线程安全
→ 错!独占是语法层面禁止拷贝,不代表多线程并发修改安全 ,写同一个 unique_ptr 是风险最高的场景。
误区 2:shared_ptr 的计数是原子安全的,所以整体线程安全
→ 错!计数安全 ≠ 对象安全,写同一个 shared_ptr 对象依然不安全,需要加锁。
误区 3:weak_ptr 是弱引用,所以比 shared_ptr 更线程安全
→ 错!两者线程安全规则完全一致,weak_ptr 只是「风险更低」(出错只返回空指针),并非「更安全」。
误区 4:用智能指针就能保证堆资源的线程安全
→ 错!所有智能指针都不保证资源的线程安全,资源的读写安全永远由开发者自己保证,智能指针只管理内存释放,不管线程竞争。
通用解决方案
方案 1:解决「指针对象的并发写」→ 加互斥锁 std::mutex
对同一个智能指针的所有写操作(赋值、reset、move),用 std::lock_guard<std::mutex> 包裹,保证同一时间只有一个线程操作该指针,彻底杜绝数据竞争。
方案 2:解决「堆资源的并发读写」→ 加互斥锁 std::mutex
对堆资源的所有读写操作加锁,不管用的是哪个智能指针、不管是同一个还是不同的指针,只要操作同一份资源,就加锁保护。
- 尽量让每个线程持有独立的智能指针对象 :拷贝
shared_ptr/weak_ptr开销极小,unique_ptr用std::move转移所有权,这样无需加锁;- 能用
unique_ptr就不用shared_ptr:unique_ptr更高效、无计数开销、线程安全问题更少;- 出现循环引用时,用
weak_ptr配合shared_ptr,这是唯一的最优解。