shared_ptr 线程安全性和最佳实践详解
目录
shared_ptr 原理和使用
什么是 shared_ptr?
std::shared_ptr 是 C++11 引入的智能指针,用于管理动态分配的对象。它通过引用计数机制实现多个 shared_ptr 实例共享同一个对象的所有权。
内部结构
shared_ptr 的内部结构(简化)如下:
cpp
template<typename T>
class shared_ptr {
private:
T* ptr_; // 指向管理的对象
ControlBlock* ctrl_; // 控制块(引用计数、弱引用计数等)
struct ControlBlock {
std::atomic<size_t> ref_count; // 引用计数(原子操作)
std::atomic<size_t> weak_count; // 弱引用计数(原子操作)
// ... 其他元数据
};
};
基本使用
cpp
#include <memory>
// 创建 shared_ptr
auto ptr1 = std::make_shared<int>(42);
auto ptr2 = ptr1; // 拷贝,引用计数 +1
auto ptr3 = ptr1; // 拷贝,引用计数 +1
// 此时引用计数为 3
// 当所有 shared_ptr 都销毁时,对象才会被释放
引用计数机制
cpp
{
auto p1 = std::make_shared<MyClass>(); // ref_count = 1
{
auto p2 = p1; // ref_count = 2
{
auto p3 = p1; // ref_count = 3
} // p3 销毁,ref_count = 2
} // p2 销毁,ref_count = 1
} // p1 销毁,ref_count = 0,对象被释放
多线程安全性分析
核心问题:shared_ptr 是线程安全的吗?
简短回答 :部分线程安全。
- ✅ 引用计数操作是线程安全的(原子操作)
- ❌ 同一个 shared_ptr 对象的读写不是线程安全的(需要同步)
1. 引用计数的线程安全性 ✅
多个 shared_ptr 副本可以在不同线程中安全使用:
cpp
// ✅ 安全:多个 shared_ptr 副本在不同线程中使用
std::shared_ptr<MyClass> global_ptr = std::make_shared<MyClass>();
// 线程 1
void thread1() {
auto local_ptr = global_ptr; // 拷贝,引用计数原子递增
local_ptr->DoSomething(); // 使用本地副本
}
// 线程 2
void thread2() {
auto local_ptr = global_ptr; // 拷贝,引用计数原子递增
local_ptr->DoSomething(); // 使用本地副本
}
为什么安全?
shared_ptr的拷贝构造函数和析构函数使用原子操作来修改引用计数- 多个线程同时拷贝或销毁不同的
shared_ptr副本是安全的 - 管理的对象会在最后一个
shared_ptr销毁时被释放
2. 同一个 shared_ptr 对象的并发访问 ❌
同一个 shared_ptr 对象不能在没有同步的情况下被多个线程同时读写:
cpp
// ❌ 不安全:多个线程同时读写同一个 shared_ptr 对象
std::shared_ptr<MyClass> shared_ptr_obj = std::make_shared<MyClass>();
// 线程 1:写操作
void thread1() {
shared_ptr_obj = std::make_shared<MyClass>(); // 写操作
}
// 线程 2:读操作
void thread2() {
auto ptr = shared_ptr_obj; // 读操作
ptr->DoSomething();
}
为什么不安全?
shared_ptr对象本身不是原子的- 多个线程同时读写同一个
shared_ptr对象会导致数据竞争(data race) - 可能导致未定义行为
3. 管理的对象的线程安全性
重要 :shared_ptr 的线程安全性只保证引用计数操作,不保证管理的对象本身的线程安全性。
cpp
// shared_ptr 本身是安全的,但管理的对象可能不安全
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
// 线程 1
void thread1() {
auto local_ptr = ptr; // ✅ shared_ptr 操作安全
local_ptr->ModifyData(); // ❌ 如果 MyClass 不是线程安全的,这里不安全
}
// 线程 2
void thread2() {
auto local_ptr = ptr; // ✅ shared_ptr 操作安全
local_ptr->ModifyData(); // ❌ 如果 MyClass 不是线程安全的,这里不安全
}
常见误区和陷阱
误区 1:认为 shared_ptr 完全线程安全
cpp
// ❌ 错误理解
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
// 错误:认为可以安全地并发读写同一个 shared_ptr 对象
void thread1() {
ptr = std::make_shared<MyClass>(); // 不安全!
}
void thread2() {
auto p = ptr; // 不安全!
}
正确做法:使用互斥锁保护
cpp
// ✅ 正确做法
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::mutex mtx;
void thread1() {
std::lock_guard<std::mutex> lock(mtx);
ptr = std::make_shared<MyClass>(); // 安全
}
void thread2() {
std::lock_guard<std::mutex> lock(mtx);
auto p = ptr; // 安全
// 解锁后使用 p,不需要锁
p->DoSomething();
}
误区 2:认为管理的对象自动线程安全
cpp
// ❌ 错误理解
std::shared_ptr<std::vector<int>> vec = std::make_shared<std::vector<int>>();
// 错误:认为 shared_ptr 保证了 vector 的线程安全
void thread1() {
auto local_vec = vec;
local_vec->push_back(1); // 不安全!vector 不是线程安全的
}
void thread2() {
auto local_vec = vec;
local_vec->push_back(2); // 不安全!
}
正确做法:为管理的对象添加同步机制
cpp
// ✅ 正确做法
std::shared_ptr<std::vector<int>> vec = std::make_shared<std::vector<int>>();
std::mutex vec_mtx;
void thread1() {
auto local_vec = vec; // shared_ptr 操作安全
std::lock_guard<std::mutex> lock(vec_mtx);
local_vec->push_back(1); // 现在安全了
}
误区 3:在析构函数中直接访问 shared_ptr 成员
cpp
// ❌ 可能不安全
class MyClass {
std::shared_ptr<OtherClass> member_ptr_;
public:
~MyClass() {
if (member_ptr_ != nullptr) { // 可能崩溃!
member_ptr_->DoSomething();
}
}
};
问题 :在析构过程中,member_ptr_ 可能正在析构,直接访问可能不安全。
正确做法:先创建本地副本
cpp
// ✅ 安全做法
class MyClass {
std::shared_ptr<OtherClass> member_ptr_;
public:
~MyClass() {
auto local_ptr = member_ptr_; // 创建本地副本
if (local_ptr != nullptr) {
local_ptr->DoSomething(); // 安全
}
}
};
最佳实践
1. 多线程环境下的 shared_ptr 使用
✅ 推荐做法:每个线程使用自己的副本
cpp
class ThreadSafeManager {
private:
std::shared_ptr<Resource> resource_;
std::mutex mtx_; // 保护 resource_ 的读写
public:
std::shared_ptr<Resource> GetResource() {
std::lock_guard<std::mutex> lock(mtx_);
return resource_; // 返回副本,解锁后使用
}
void SetResource(std::shared_ptr<Resource> new_resource) {
std::lock_guard<std::mutex> lock(mtx_);
resource_ = new_resource;
}
};
// 使用
void worker_thread(ThreadSafeManager& manager) {
auto resource = manager.GetResource(); // 获取副本
// 解锁后使用 resource,不需要锁
resource->DoWork();
}
✅ 推荐做法:使用 atomic<shared_ptr>(C++20)
cpp
#include <atomic>
#include <memory>
// C++20 提供了 std::atomic<std::shared_ptr<T>>
std::atomic<std::shared_ptr<MyClass>> atomic_ptr;
// 线程 1
void thread1() {
atomic_ptr.store(std::make_shared<MyClass>()); // 原子操作
}
// 线程 2
void thread2() {
auto ptr = atomic_ptr.load(); // 原子操作
ptr->DoSomething();
}
注意 :std::atomic<std::shared_ptr<T>> 只在 C++20 及以后版本可用。
2. 析构函数中的 shared_ptr 使用
✅ 推荐做法:先创建本地副本
cpp
class MyClass {
std::shared_ptr<Dependency> dependency_;
public:
~MyClass() {
// 先创建本地副本,确保安全
auto dep = dependency_;
if (dep != nullptr) {
dep->Cleanup();
}
}
};
✅ 推荐做法:多层 shared_ptr 访问
cpp
class MyClass {
std::shared_ptr<Manager> manager_;
public:
~MyClass() {
// 多层访问,每一层都创建副本
auto mgr = manager_;
if (mgr != nullptr) {
auto handler = mgr->GetHandler();
if (handler != nullptr) {
handler->Cleanup();
}
}
}
};
3. 避免循环引用
cpp
// ❌ 循环引用导致内存泄漏
class Parent {
std::shared_ptr<Child> child_;
};
class Child {
std::shared_ptr<Parent> parent_; // 循环引用!
};
// 使用
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child_ = child;
child->parent_ = parent; // 循环引用,无法释放
解决方案 :使用 weak_ptr 打破循环
cpp
// ✅ 使用 weak_ptr 打破循环
class Parent {
std::shared_ptr<Child> child_;
};
class Child {
std::weak_ptr<Parent> parent_; // 使用 weak_ptr
};
// 使用
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child_ = child;
child->parent_ = parent; // weak_ptr 不会增加引用计数
// parent 和 child 可以正常释放
4. 性能考虑
✅ 推荐:使用 make_shared
cpp
// ✅ 推荐:一次内存分配
auto ptr = std::make_shared<MyClass>(arg1, arg2);
// ❌ 不推荐:两次内存分配
auto ptr = std::shared_ptr<MyClass>(new MyClass(arg1, arg2));
优势:
make_shared将对象和控制块分配在同一块内存中- 减少内存分配次数,提高性能
- 更好的异常安全性
✅ 推荐:避免不必要的拷贝
cpp
// ❌ 不必要的拷贝
void Process(std::shared_ptr<Data> data) {
// 如果不需要修改 data,使用 const&
}
// ✅ 推荐:按需选择参数类型
void Process(const std::shared_ptr<Data>& data) {
// 不需要拷贝,直接使用引用
}
// 或者
void Process(std::shared_ptr<Data> data) {
// 如果需要延长生命周期,使用值传递
}
5. 线程安全的单例模式
cpp
class Singleton {
private:
static std::shared_ptr<Singleton> instance_;
static std::mutex mtx_;
Singleton() = default;
public:
static std::shared_ptr<Singleton> GetInstance() {
// 双重检查锁定
if (instance_ == nullptr) {
std::lock_guard<std::mutex> lock(mtx_);
if (instance_ == nullptr) {
instance_ = std::make_shared<Singleton>();
}
}
return instance_; // 返回副本,线程安全
}
};
std::shared_ptr<Singleton> Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;
总结
shared_ptr 线程安全性要点
-
引用计数操作是线程安全的
- 多个
shared_ptr副本可以在不同线程中安全使用 - 拷贝和销毁操作使用原子操作
- 多个
-
同一个 shared_ptr 对象不是线程安全的
- 多个线程同时读写同一个
shared_ptr对象需要同步 - 使用互斥锁或
atomic<shared_ptr>(C++20)
- 多个线程同时读写同一个
-
管理的对象的线程安全性是独立的
shared_ptr只保证引用计数的线程安全- 管理的对象本身需要额外的同步机制
最佳实践总结
- ✅ 多线程环境 :每个线程使用自己的
shared_ptr副本 - ✅ 析构函数:先创建本地副本再使用
- ✅ 避免循环引用 :使用
weak_ptr打破循环 - ✅ 性能优化 :使用
make_shared,避免不必要的拷贝 - ✅ 同步机制 :需要时使用互斥锁保护
shared_ptr对象
关键原则
记住 :
shared_ptr的线程安全性只保证引用计数操作,不保证对象本身的线程安全性,也不保证同一个shared_ptr对象的并发访问安全。
遵循这些原则和最佳实践,可以安全、高效地使用 shared_ptr 进行多线程编程。