在 C++17 及以后,std::shared_ptr 已经对多线程使用做了很好的内置支持。它通过对引用计数使用原子操作来保证线程安全,使得不同线程可以共用同一个 shared_ptr 对象而不需要额外同步。下面从内部实现、常见误区和最佳实践三方面详细剖析如何在多线程场景下安全使用 std::shared_ptr。
1. 内部实现原理
1.1 引用计数是原子的
std::shared_ptr 的引用计数(use_count_)通常是一个 `std::atomic
`。无论是 `use_count()`、`unique()`、`reset()` 还是拷贝构造、赋值,都只会对计数执行一次原子加/减操作,从而保证计数的原子性。这样即使多个线程同时拷贝或销毁同一个 `shared_ptr`,计数也不会出现数据竞争。
### 1.2 控制块与控制块分离
C++20 起引入了 **控制块分离**(Control Block Separation)模式。`std::shared_ptr` 的控制块只持有引用计数与删除器,而实际对象的生命周期由 `shared_ptr` 或 `weak_ptr` 控制。由于控制块独立存储,线程安全机制只需要保护控制块,而不需要关心对象本身的内部状态。
—
## 2. 常见误区
| 误区 | 解释 | 解决方案 |
|——|——|———-|
| **“只要引用计数安全,数据本身也安全”** | `shared_ptr` 只保证计数本身线程安全,指向的对象本身并不安全。若对象内部有可变状态,仍需同步。 | 对对象内部状态使用 `std::mutex`、`std::atomic` 或其他同步原语。 |
| **“多线程直接访问 `shared_ptr` 的 `operator->` 或 `operator*` 就安全”** | 即使计数安全,两个线程可能同时使用同一个对象,对象内部的并发写可能冲突。 | 只在读操作或不冲突的写操作时使用;必要时使用外部锁或 `std::shared_mutex`。 |
| **“复制一个 `shared_ptr` 就能得到完全隔离的资源”** | 复制只复制引用计数,并不复制对象。所有 `shared_ptr` 仍指向同一对象。 | 若需要独立副本,使用 `std::make_shared` 或 `std::shared_ptr
::make_shared`,并在构造时深拷贝。 |
| **“`reset()` 与 `swap()` 在多线程下不安全”** | 这些操作内部都调用原子加/减计数,并更新指针,已是线程安全的。 | 只要没有外部对同一 `shared_ptr` 进行竞争,调用是安全的。 |
—
## 3. 最佳实践
### 3.1 只在需要共享时使用 `shared_ptr`
如果对象只在单线程或已同步的线程间共享,使用 `std::unique_ptr` 更轻量。仅当确实需要多处存活引用时才切换为 `shared_ptr`。
### 3.2 采用 `std::make_shared` 而非单独 `new`
`make_shared` 通过一次分配同时创建对象和控制块,减少碎片并提高 cache locality。它的内部实现已经考虑了多线程计数的原子性。
### 3.3 对内部可变状态使用同步
如果对象内部有可变成员,建议:
– 将可变成员声明为 `std::atomic`(适合简单标志或计数)
– 使用 `std::mutex` 或 `std::shared_mutex` 对更复杂状态进行互斥访问
– 对于读多写少的情况,可使用 `std::shared_mutex`(读写锁)
### 3.4 减少 `shared_ptr` 的拷贝
拷贝 `shared_ptr` 需要对计数进行原子加,虽然性能很高,但在高并发场景下仍有开销。可考虑:
– 将 `shared_ptr` 作为 `const std::shared_ptr
&` 传递,避免不必要的拷贝
– 对于临时使用,可使用 `std::shared_ptr
` 的 `make_shared` 并在需要时使用 `std::move`
### 3.5 防止循环引用
循环引用会导致内存泄漏。使用 `std::weak_ptr` 破坏循环,尤其在树或图结构中尤为重要。示例:
“`cpp
struct Node {
std::vector<std::shared_ptr> children;
std::weak_ptr
parent; // 破坏循环
};
“`
### 3.6 在多线程环境中使用 `shared_ptr` 的例子
“`cpp
#include
#include
#include
#include
#include
struct Data {
int value;
std::mutex mtx; // 保护内部状态
};
void worker(std::shared_ptr
sp) {
for (int i = 0; i < 5; ++i) {
std::lock_guard lock(sp->mtx);
++sp->value;
std::cout << "Thread " << std::this_thread::get_id() << " incremented to " <value << '\n';
}
}
int main() {
auto data = std::make_shared
();
data->value = 0;
std::vector ths;
for (int i = 0; i < 4; ++i)
ths.emplace_back(worker, data); // 共享同一个 shared_ptr
for (auto& t : ths) t.join();
std::cout << "Final value: " <value << '\n';
return 0;
}
“`
在上述代码中:
– `shared_ptr` 计数保证线程安全
– 对象内部的 `mtx` 防止并发写冲突
—
## 4. 结语
`std::shared_ptr` 在多线程环境中提供了非常可靠的引用计数机制,开发者无需担心计数本身的数据竞争。但它并不自动保护对象内部状态。合理使用 `std::make_shared`、`std::weak_ptr` 以及合适的同步原语,能够让 `shared_ptr` 成为多线程编程中的强大工具。祝你编码愉快!</std::shared_ptr