网站首页 > 技术文章 正文
C++ 智能指针的线程安全:堆与栈的 "室友守则"
一、先搞懂:栈和堆像什么?
想象你住的公寓楼:
- 栈就是你自己的卧室,东西(变量)都是你的私产,别人(其他线程)进不来,操作自己的东西永远安全。
- 堆就是公寓的公共厨房,所有住户(线程)都能进,这里的东西(动态分配的对象)谁都能碰,容易出乱子。
智能指针呢?它就像你在厨房放的 "共享工具":指针本身(比如shared_ptr<int> sp这个变量)在你的卧室(栈)里,是私有的;但它指向的工具(堆上的int对象)和 "借用登记本"(shared_ptr的控制块,记录有多少人在用这个工具)在公共厨房(堆)里,是共享的。
二、智能指针的线程安全:"登记本" 和 "工具" 要分开看
C++ 标准规定了智能指针的线程安全规则,简单说:
- 控制块(引用计数)是线程安全的:多个线程同时 "借" 或 "还" 工具(复制或销毁shared_ptr),登记本(引用计数)不会算错(原子操作保证)。
- 指向的对象(堆上数据)是线程不安全的:多个线程同时用这个工具(修改对象),会打架(数据竞争),得自己加锁。
- unique_ptr比较特殊:它是 "独用工具",不能复制只能转移,就像你的专属菜刀,从不让别人碰,所以只要别强行共享(比如传裸指针),就不会有线程安全问题。
三、案例说话:代码跑起来才明白
案例 1:shared_ptr的引用计数线程安全
场景:10 个线程同时复制同一个shared_ptr,最后看引用计数是否正确。
cpp
运行
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
// 全局的共享指针,指向一个int(值不重要,看引用计数)
std::shared_ptr<int> global_sp = std::make_shared<int>(0);
// 线程函数:复制shared_ptr
void copy_shared_ptr() {
// 每个线程复制一次全局shared_ptr,存在栈上(线程私有)
std::shared_ptr<int> local_sp = global_sp;
// 稍等一下,模拟实际工作
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
int main() {
std::cout << "初始引用计数:" << global_sp.use_count() << std::endl; // 应该是1
std::vector<std::thread> threads;
// 创建10个线程
for (int i = 0; i < 10; ++i) {
threads.emplace_back(copy_shared_ptr);
}
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
std::cout << "所有线程结束后,引用计数:" << global_sp.use_count() << std::endl; // 应该还是1(因为local_sp都销毁了)
return 0;
}
编译运行:
bash
# 编译(需要链接线程库,-pthread)
g++ -std=c++11 -pthread shared_ptr_refcount.cpp -o refcount_demo
# 运行
./refcount_demo
预期输出:
plaintext
初始引用计数:1
所有线程结束后,引用计数:1
解释:10 个线程各自复制了global_sp(引用计数临时涨到 11),但线程结束后局部的local_sp销毁(引用计数减回 1)。整个过程中引用计数的增减是原子操作,不会算错 —— 这就是控制块的线程安全。
案例 2:shared_ptr指向的对象线程不安全
场景:10 个线程同时用shared_ptr修改它指向的int,不加锁会出什么乱子?
cpp
运行
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <mutex>
// 共享的智能指针,指向一个计数器
std::shared_ptr<int> counter_sp = std::make_shared<int>(0);
// 准备一个锁(后面会用到)
std::mutex mtx;
// 线程函数:不加锁修改对象
void increment_without_lock() {
for (int i = 0; i < 1000; ++i) {
// 直接修改共享对象(危险!)
(*counter_sp)++;
}
}
// 线程函数:加锁修改对象
void increment_with_lock() {
for (int i = 0; i < 1000; ++i) {
// 加锁保护共享对象
std::lock_guard<std::mutex> lock(mtx);
(*counter_sp)++;
}
}
int main() {
// 测试不加锁的情况
*counter_sp = 0; // 重置计数器
std::vector<std::thread> threads1;
for (int i = 0; i < 10; ++i) {
threads1.emplace_back(increment_without_lock);
}
for (auto& t : threads1) {
t.join();
}
std::cout << "不加锁,预期10000,实际:" << *counter_sp << std::endl;
// 测试加锁的情况
*counter_sp = 0; // 重置计数器
std::vector<std::thread> threads2;
for (int i = 0; i < 10; ++i) {
threads2.emplace_back(increment_with_lock);
}
for (auto& t : threads2) {
t.join();
}
std::cout << "加锁,预期10000,实际:" << *counter_sp << std::endl;
return 0;
}
编译运行:
bash
g++ -std=c++11 -pthread shared_ptr_obj.cpp -o obj_demo
./obj_demo
预期输出:
plaintext
不加锁,预期10000,实际:9527(每次可能不同,小于10000)
加锁,预期10000,实际:10000
解释:不加锁时,多个线程同时读写*counter_sp,会出现 "读脏数据"(比如两个线程同时读到 5,都加 1 变成 6,实际应该是 7)。这说明:智能指针本身的引用计数安全,但它指的对象,该加锁还得加锁!
案例 3:unique_ptr的线程安全(别共享就没事)
场景:unique_ptr不能复制,只能通过移动转移所有权,多线程中安全传递。
cpp
运行
#include <iostream>
#include <memory>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
// 用队列和条件变量安全传递unique_ptr
std::queue<std::unique_ptr<int>> q;
std::mutex q_mtx;
std::condition_variable cv;
// 生产者线程:创建unique_ptr并放入队列
void producer() {
for (int i = 0; i < 5; ++i) {
// 创建unique_ptr(在生产者线程的栈上)
auto ptr = std::make_unique<int>(i);
std::cout << "生产:" << *ptr << std::endl;
// 加锁放入队列(转移所有权)
std::lock_guard<std::mutex> lock(q_mtx);
q.push(std::move(ptr)); // 必须用move,因为unique_ptr不能复制
}
// 通知消费者
cv.notify_one();
}
// 消费者线程:从队列取unique_ptr并使用
void consumer() {
std::unique_lock<std::mutex> lock(q_mtx);
// 等待队列有数据
cv.wait(lock, []{ return !q.empty(); });
// 取出并使用
while (!q.empty()) {
auto ptr = std::move(q.front()); // 转移所有权到消费者线程
q.pop();
std::cout << "消费:" << *ptr << std::endl;
}
}
int main() {
std::thread prod(producer);
std::thread cons(consumer);
prod.join();
cons.join();
return 0;
}
编译运行:
bash
g++ -std=c++11 -pthread unique_ptr_demo.cpp -o unique_demo
./unique_demo
预期输出:
plaintext
生产:0
生产:1
生产:2
生产:3
生产:4
消费:0
消费:1
消费:2
消费:3
消费:4
解释:unique_ptr像 "一次性餐具",只能一个人用,用完传给下个人(通过std::move)。只要不强行复制(编译器会报错),多线程传递时就很安全 —— 毕竟 "独一份" 的东西,不会有人抢。
四、总结:智能指针的 "安全手册"
- shared_ptr的引用计数(控制块)是线程安全的,随便复制销毁不用慌。
- shared_ptr指向的对象,该加锁加锁,别指望智能指针帮你管对象的线程安全。
- unique_ptr只要不共享(别传裸指针给多个线程),天生线程安全,因为 "独占" 就是最好的保护。
标题
- 《C++ 智能指针线程安全:堆与栈的 "共享潜规则"》
- 《从厨房到卧室:用生活理解智能指针的线程安全》
简介
本文用生活化类比解析 C++ 智能指针的线程安全特性,从栈(线程私有)和堆(线程共享)的角度,说明shared_ptr的引用计数安全与指向对象的不安全,以及unique_ptr的独占式安全。通过多个可运行案例展示具体场景,帮助开发者轻松掌握智能指针在多线程中的正确用法。
关键词
#C++ 智能指针 #线程安全 #堆内存 #栈内存 #shared_ptr
猜你喜欢
- 2025-10-23 分享50道Java多线程高频面试题,面试不用愁
- 2025-10-23 CopyOnWriteArrayList 读写分离,弱一致性
- 2024-08-12 ArrayList线程不安全的案例分析(arraylist线程不安全为什么还要用)
- 2024-08-12 阿里P7告诉你什么是java并发包、线程池、锁
- 2024-08-12 深度解析CopyOnWriteArrayList,线程安全版
- 2024-08-12 Java 最细的集合类总结(java常见的集合类)
- 2024-08-12 面试必问之 CopyOnWriteArrayList,你了解多少?
- 2024-08-12 击破百度、腾讯、阿里招聘套路!百家IT名企面试真题新鲜出炉
- 2024-08-12 简单定义多线程(多线程编写)
- 2024-08-12 java集合间的小比较(java集合对比)
- 最近发表
-
- 聊一下 gRPC 的 C++ 异步编程_grpc 异步流模式
- [原创首发]安全日志管理中心实战(3)——开源NIDS之suricata部署
- 超详细手把手搭建在ubuntu系统的FFmpeg环境
- Nginx运维之路(Docker多段构建新版本并增加第三方模
- 92.1K小星星,一款开源免费的远程桌面,让你告别付费远程控制!
- Go 人脸识别教程_piwigo人脸识别
- 安卓手机安装Termux——搭建移动服务器
- ubuntu 安装开发环境(c/c++ 15)_ubuntu安装c++编译器
- Rust开发环境搭建指南:从安装到镜像配置的零坑实践
- Windows系统安装VirtualBox构造本地Linux开发环境
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (77)
- vector线程安全吗 (73)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)
