自定义SharedPtr实现是否符合标准规定的线程安全性?
我了解std::shared_ptr具备一定的线程安全性,核心保证是:当多个shared_ptr实例同时销毁时,指向的对象只会被销毁一次。而智能指针的其他操作(如拷贝、移动等)并不具备线程安全性,这个设计是合理的——毕竟没有显式互斥锁的话,很难实现这类线程安全。
现在问题是:下面的SharedPtr实现是否符合C++标准规定的线程安全性要求?
#include <atomic> #include <utility> template<class T> struct SharedPtr { private: std::atomic<int> *pRefCnt_ = nullptr; T *pObj_ = nullptr; void destroy() { if (pRefCnt_ && *pRefCnt_-- == 0) { delete std::exchange(pRefCnt_, nullptr); delete std::exchange(pObj_, nullptr); } } void copy(SharedPtr const &other) { pRefCnt_ = other.pRefCnt_; pObj_ = other.pObj_; if (pRefCnt_) { ++*pRefCnt_; } } void move(SharedPtr &&other) { pRefCnt_ = std::exchange(other.pRefCnt_, nullptr); pObj_ = std::exchange(other.pObj_, nullptr); } public: SharedPtr() = default; explicit SharedPtr(T *p) : pRefCnt_(new std::atomic<int>(0)), pObj_(p) {} SharedPtr(SharedPtr const &rhs) noexcept { copy(rhs); } SharedPtr(SharedPtr &&rhs) noexcept { move(std::move(rhs)); } SharedPtr &operator=(SharedPtr const &rhs) noexcept { if (this != &rhs) { destroy(); copy(rhs); } return *this; } SharedPtr &operator=(SharedPtr &&rhs) noexcept { if (this != &rhs) { destroy(); move(std::move(rhs)); } return *this; } ~SharedPtr() { destroy(); } T *operator*() const { return pObj_; } T &operator->() const { return *pObj_; } explicit operator bool() const { return pObj_ != nullptr; } };
核心问题分析
这个自定义SharedPtr的实现不符合C++标准对shared_ptr线程安全性的要求,主要存在以下几个关键问题:
引用计数递减的原子性问题
destroy()函数中的*pRefCnt_-- == 0是致命错误:这行代码会先读取原子变量的当前值,再执行递减操作,两个步骤并非原子的。当多线程同时销毁指向同一对象的SharedPtr时,可能出现多个线程都读取到引用计数为1,随后都执行递减,最终导致引用计数变为负数,且多个线程都会进入销毁分支,重复释放对象和引用计数指针,触发未定义行为。
正确的做法是使用std::atomic::fetch_sub原子操作,确保判断和递减的原子性:if (pRefCnt_ && pRefCnt_->fetch_sub(1) == 1) { delete std::exchange(pRefCnt_, nullptr); delete std::exchange(pObj_, nullptr); }构造函数引用计数初始化错误
使用原始指针构造SharedPtr时,引用计数被初始化为0,但正确的初始值应该是1——当前SharedPtr已经持有对象的引用,计数从1开始才符合逻辑。这个错误会导致第一个销毁的SharedPtr就触发对象销毁,此时若还有其他SharedPtr持有该对象,会直接产生悬空指针。拷贝操作的竞态条件
copy()函数中先赋值pRefCnt_ = other.pRefCnt_,再执行++*pRefCnt_。如果另一个线程刚好在赋值完成后、递增操作前销毁了other对应的SharedPtr并释放了pRefCnt_,当前线程对*pRefCnt_的递增就会访问已释放的内存,引发未定义行为。正确逻辑应该是先对other的引用计数做原子递增,再赋值给自己的指针,确保计数递增完成前,引用计数不会被释放。
总结
该实现存在多处线程安全漏洞,无法满足C++标准中shared_ptr的核心线程安全保证——多线程销毁时对象仅被销毁一次。要符合标准,必须保证引用计数的增减都是原子复合操作,同时修正初始化和拷贝逻辑中的竞态问题。
内容的提问来源于stack exchange,提问作者Aykhan Hagverdili

