std::shared_ptr 的线程安全性是怎样的?
std::shared_ptr 的线程安全性是一个在 C++ 面试和实际开发中非常经典且容易产生误解的问题。
简单来说:std::shared_ptr 的引用计数是线程安全的,但 std::shared_ptr 对象本身的读写不是线程安全的,它所指向的对象的读写也不是线程安全的。
为了彻底理解,我们需要把它的线程安全性拆解为三个独立的层面来看:
1. 引用计数(控制块)的线程安全性:绝对安全
std::shared_ptr 在底层由两部分组成:指向对象的指针,以及指向控制块(Control Block)的指针。控制块中包含了强引用计数和弱引用计数。
- 行为: 标准库保证了对控制块中引用计数的增加(如拷贝
shared_ptr)和减少(如shared_ptr析构)都是原子操作(Atomic)。 - 结论: 多个线程可以安全地各自拥有指向同一个底层对象的不同的
shared_ptr实例。如果多个线程同时销毁它们各自的shared_ptr,引用计数的递减是安全的,且对象只会恰好被销毁一次。
2. shared_ptr 对象本身的线程安全性:不安全(存在数据竞争)
标准库中对 shared_ptr 实例的线程安全级别,等同于对普通内置类型(如 int 或裸指针)的线程安全级别:多读安全,读写、多写不安全。
- 行为:
shared_ptr内部通常包含两个裸指针(一个指对象,一个指控制块)。当你修改一个shared_ptr时(比如给它赋新值),这两个指针都需要被修改。这不是一个原子操作。 - 危险场景: 如果线程 A 和 线程 B 同时去修改同一个
shared_ptr实例(或者一个读、一个写),可能会发生“指针撕裂(Tearing)”。比如线程 A 刚更新了对象指针,还没更新控制块指针,此时线程 B 读到了这个中间状态,就会导致程序崩溃。 - 结论: 不能在没有同步机制(如
std::mutex)保护的情况下,多线程同时读写同一个shared_ptr变量。
3. 被管理对象的线程安全性:不安全(取决于对象自身)
- 行为:
shared_ptr只负责管理对象的生命周期,不负责对象内容的线程同步。 - 结论: 如果多个线程通过各自独立的
shared_ptr同时访问底层对象的成员变量,如果发生并发读写,依然会产生数据竞争。你需要自己用互斥锁、原子变量等机制来保护被管理的对象。
具体场景举例
✅ 安全场景 1:多线程读取同一个 shared_ptr
std::shared_ptr<int> p = std::make_shared<int>(10);
// 线程1和线程2同时只读 p,绝对安全
std::thread t1([&]{ int a = *p; });
std::thread t2([&]{ std::shared_ptr<int> p2 = p; }); // 拷贝会修改控制块,但不会修改 p 本身
✅ 安全场景 2:多线程操作指向同一个对象的不同 shared_ptr 副本
std::shared_ptr<int> p = std::make_shared<int>(10);
std::shared_ptr<int> p1 = p; // 给线程1用
std::shared_ptr<int> p2 = p; // 给线程2用
// 线程1和线程2操作各自的 p1 和 p2,安全(因为引用计数的增减是原子的)
std::thread t1([&]{ p1.reset(); });
std::thread t2([&]{ p2 = std::make_shared<int>(20); });
❌ 危险场景:多线程读写同一个 shared_ptr 实例
std::shared_ptr<int> p = std::make_shared<int>(10);
// 线程1和线程2同时读写变量 p,【数据竞争!会导致崩溃!】
std::thread t1([&]{ p = std::make_shared<int>(20); });
std::thread t2([&]{ std::shared_ptr<int> p2 = p; });
如何解决多线程读写同一个 shared_ptr 的问题?
如果你确实需要在多个线程之间共享并修改同一个 shared_ptr 变量,有以下几种解法:
使用互斥锁 (
std::mutex):
最传统的做法,在每次读写该shared_ptr时加锁。使用 C++11 的原子操作特化(C++20 已废弃):
cppstd::shared_ptr<int> p; // 写 std::atomic_store(&p, std::make_shared<int>(10)); // 读 std::shared_ptr<int> p2 = std::atomic_load(&p);使用 C++20 的
std::atomic<std::shared_ptr<T>>(推荐):
C++20 正式引入了shared_ptr的原子模板特化,这是目前最优雅的解决方式。cppstd::atomic<std::shared_ptr<int>> atomic_p; // 线程1 atomic_p.store(std::make_shared<int>(10)); // 线程2 std::shared_ptr<int> local_p = atomic_p.load();
总结
面试时回答这个问题的黄金句式:“shared_ptr 的引用计数操作是线程安全的,但对同一个 shared_ptr 对象本身的并发读写不是线程安全的。如果要在多线程下操作同一个 shared_ptr 变量,需要使用 mutex 或 C++20 的 std::atomic<std::shared_ptr>。同时,它管理的底层对象的线程安全性需要开发者自己保证。”