C++ 中如何安全地使用 std::shared_ptr 在多线程环境中?

在 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

发表评论