unique_ptr shared_ptr weak_ptr的线程安全问题

std::unique_ptrstd::shared_ptrstd::weak_ptr 全部都是「部分线程安全」没有任何一个是完全线程安全的,也没有任何一个是「完全不安全」的;

三者的线程安全特性差异极大 ,核心根源是:设计初衷不同、内存管理方式不同、是否包含引用计数、是否独占资源

补充:所有 C++ 标准库智能指针的线程安全规则,是C++ 标准强制规定的,GCC/Clang/MSVC 所有编译器实现完全一致,无版本差异。

根本原则

所有智能指针,只保证「智能指针对象自身」的线程安全边界永远不保证「其指向的堆内存资源」的线程安全

智能指针不是「线程安全的银弹」:多个线程通过任意智能指针操作同一份堆内存资源时,资源的读写安全,完全由开发者自己保证(加锁),和用什么智能指针无关,和裸指针的规则完全一致。

  1. 多线程只读 同一份堆资源 → 线程安全,无需加锁;
  2. 多线程读写 / 写写 同一份堆资源 → 线程不安全,必须加锁std::mutex),否则数据竞争、内存错乱、程序崩溃;

一、std::unique_ptr 线程安全特性(独占型智能指针)

核心特性
  1. 独占所有权 :一个 unique_ptr 独占一份堆资源,同一时间只能有一个 unique_ptr 指向该资源 ,不允许拷贝,只能移动(std::move);
  2. 无引用计数unique_ptr 是轻量级智能指针,内部只有一个裸指针,没有任何引用计数,也不会在堆上开辟额外内存;
  3. 极致高效:所有操作都是编译期的指针拷贝 / 移动,无运行时开销。
规则 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」的场景,因为它的设计初衷就是「独占」,一般是:

  1. 单个线程内创建、使用、销毁 unique_ptr
  2. 通过 std::move 将所有权「转移」给其他线程,转移后原线程的 unique_ptr 就为空了,不会再操作。→ 这一特点让 unique_ptr 在工程中几乎不会出现线程安全问题。

二、std::shared_ptr 线程安全特性(共享型智能指针)

核心特性
  1. 共享所有权 :多个 shared_ptr 可以指向同一份堆资源,互相不冲突;
  2. 双引用计数 :堆上开辟独立的「计数块」,包含 强引用计数 (use_count)弱引用计数 (weak_count) ,所有计数的增减都是原子操作(CPU 原子指令实现,无锁);
  3. 有运行时开销 :拷贝 / 析构时需要原子操作增减计数,比 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 存在)

核心特性
  1. 无所有权weak_ptrshared_ptr 的「附属品」,不持有资源的强引用,不会影响资源的生命周期,也不能直接解引用访问资源;
  2. 依附共享计数weak_ptr 绑定到 shared_ptr 的「双引用计数块」,自身包含弱引用计数,增减也是原子操作;
  3. 核心作用 :解决 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_ptrstd::move 转移所有权,这样无需加锁;
  • 能用 unique_ptr 就不用 shared_ptrunique_ptr 更高效、无计数开销、线程安全问题更少;
  • 出现循环引用时,用 weak_ptr 配合 shared_ptr,这是唯一的最优解。
相关推荐
信安成长日记2 小时前
会创建Pod的资源
安全
Howrun7772 小时前
虚幻引擎_用户小控件_准星
c++·游戏引擎·虚幻
CoderCodingNo2 小时前
【GESP】C++六级考试大纲知识点梳理, (1) 树的概念与遍历
开发语言·c++
星火开发设计2 小时前
C++ multimap 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识
●VON2 小时前
使用 OpenAgents 搭建基于智谱 GLM 的本地智能体(Agent)
学习·安全·制造·智能体·von
智驱力人工智能2 小时前
矿场轨道异物AI监测系统 构建矿山运输安全的智能感知防线 轨道异物检测 基于YOLO的轨道异物识别算法 地铁隧道轨道异物实时预警技术
人工智能·opencv·算法·安全·yolo·边缘计算
深圳市恒讯科技2 小时前
常见服务器漏洞及防护方法
服务器·网络·安全
m0_738120722 小时前
应急响应——知攻善防蓝队溯源靶机Linux-2详细流程
linux·服务器·网络·安全·web安全·php
李日灐2 小时前
C++STL:deque、priority_queue详解!!:详解原理和底层
开发语言·数据结构·c++·后端·stl