乐于分享
好东西不私藏

libc++stl源码分享-weak_ptr实现原理

libc++stl源码分享-weak_ptr实现原理
继续恢复不定时更新。最近一段时间由于AI的冲击以及自己要去研究编译器虚拟机等相关的问题 导致未能继续更新相应的公众号。
目前决定继续更新。虽然AI有很强的冲击,但我认为对于libc++stl源码的解读和分析还是无法完全依靠AI的还是需要人来主动分析,并借助AI辅助,才能真正理解透测。
这一部分我们来继续分析同shared_ptr相关的实现。主要分析下
  • libc++stl中weak_ptr是如何实现的
  • weak_ptr是如何导致shared_ptr中相关资源延迟释放的
weak_ptr的实现
下面假设基于C++17来讲解,且去掉多余的宏,具体来说基于如下条件
_LIBCPP_STD_VER = 17去掉:_LIBCPP_SHARED_PTR_TRIVIAL_ABI_LIBCPP_HIDE_FROM_ABI_LIBCPP_NODEBUG_LIBCPP_CONSTEXPR_NOEXCEPT
此时,weak_ptr的定义变为
template<class T>class weak_ptr {public:    using element_type = std::remove_extent_t<T>;private:    element_type* __ptr_;    __shared_weak_count* __cntrl_;public:    weak_ptr();    weak_ptr(shared_ptr<T> const& r);    weak_ptr(weak_ptr const& r);    weak_ptr(weak_ptr&& r);    ~weak_ptr();    weak_ptr& operator=(...);    voidswap(weak_ptr& r);    voidreset();    longuse_count()const{        return __cntrl_ ? __cntrl_->use_count() : 0;    }    boolexpired()const{        return __cntrl_==nullptr            || __cntrl_->use_count()==0;    }    shared_ptr<T> lock()const;};
总体来说weak_ptr包含两个成员,其形式如下
weak_ptr+-------------------+| __ptr_            | -----> Object(T)+-------------------+| __cntrl_          | -----> ControlBlock+-------------------+
其中ptr指向shared ptr中所管理的对象的地址,cntrl指向shared ptr的内存块。关于shared ptr的控制块相关的分析 可参考libc++ stl源码分析-揭秘智能指针shared_ptr(上)
我们来看下weak ptr是如何通过shared ptr来构造的。在源码库中weak ptr有如下构造函数
template <class _Tp>inline weak_ptr<_Tp>::weak_ptr(weak_ptr const& __r) _NOEXCEPT : __ptr_(__r.__ptr_), __cntrl_(__r.__cntrl_) {  if (__cntrl_)    __cntrl_->__add_weak();}
template <class _Tp>template <class _Yp__enable_if_t<__compatible_with<_Yp, _Tp>::value, int> >inline weak_ptr<_Tp>::weak_ptr(shared_ptr<_Yp> const& __r) _NOEXCEPT : __ptr_(__r.__ptr_), __cntrl_(__r.__cntrl_) {  if (__cntrl_)    __cntrl_->__add_weak();}
当用一个shared ptr来初始化weak ptr时,会调用控制块的__add_weak()接口。
在shared ptr源码分析中,我们讲解到shared ptr的控制块分两种
  • 通过shared ptr直接构造,此时控制块为__shared_ptr_pointer
  • 通过make shared接口创建的shared ptr,此时控制块为
    __shared_ptr_emplace
无轮是哪一种控制块,__add_weak()都实现同样的操作,增加__shared_weak_owners_的计数。
weak ptr为啥会延迟释放shared ptr的资源
这一部分我们主要研究weak ptr为啥会延迟释放shared ptr的资源。
weak ptr的析构函数为
template<class _Tp>weak_ptr<_Tp>::~weak_ptr(){    if (__cntrl_)        __cntrl_->__release_weak();}
当shared ptr的强引用计数为0时,如果此时还有weak ptr存在,那么此时shared ptr的控制块不会被释放。
首先,我们来看下shared ptr的析构函数,其函数实现为
    ~shared_ptr()    {        if (__cntrl_)            __cntrl_->__release_shared();    }
可知析构函数中会调用控制块的
__release_shared
函数,这个函数的实现为
    void __release_shared() _NOEXCEPT {      if (__shared_count::__release_shared())        __release_weak();    }
    bool __release_shared() _NOEXCEPT {      if (__libcpp_atomic_refcount_decrement(__shared_owners_) == -1) {        __on_zero_shared();        return true;      }      return false;    }
最终会调用到
__on_zero_shared();
接口,这个函数的实现为
__shared_ptr_pointer<_Tp, _Dp, _Alloc>::__on_zero_shared() _NOEXCEPT{    __data_.first().second()(__data_.first().first());    __data_.first().second().~_Dp();}
这里的
__data_.first().second()
就是Deleter,而
__data_.first().first()
就是T*
所以这个函数的实现就是简单的释放shared ptr所管理的对象所占用的内存,并没有释放控制块
我们再来看看,通过make shared创建的shared ptr的释放过程。
    virtual void __on_zero_shared() _NOEXCEPT {#if _LIBCPP_STD_VER > 17        using _TpAlloc = typename __allocator_traits_rebind<_Alloc, _Tp>::type;        _TpAlloc __tmp(*__get_alloc());        allocator_traits<_TpAlloc>::destroy(__tmp, __get_elem());#else        __get_elem()->~_Tp();#endif    }
在C++17中,shared ptr的析构函数只会调用所管理对象的析构函数 不会释放其对应的资源。
当shared ptr的强引用计数为0时,会调用__release_weak接口,这个接口的实现为
void __shared_weak_count::__release_weak() noexcept {  // NOTE: The acquire load here is an optimization of the very  // common case where a shared pointer is being destructed while  // having no other contended references.  //  // BENEFIT: We avoid expensive atomic stores like XADD and STREX  // in a common case.  Those instructions are slow and do nasty  // things to caches.  //  // IS THIS SAFE?  Yes.  During weak destruction, if we see that we  // are the last reference, we know that no-one else is accessing  // us. If someone were accessing us, then they would be doing so  // while the last shared / weak_ptr was being destructed, and  // that's undefined anyway.  //  // If we see anything other than a 0, then we have possible  // contention, and need to use an atomicrmw primitive.  // The same arguments don't apply for increment, where it is legal  // (though inadvisable) to share shared_ptr references between  // threads, and have them all get copied at once.  The argument  // also doesn't apply for __release_shared, because an outstanding  // weak_ptr::lock() could read / modify the shared count.  if (__libcpp_atomic_load(&__shared_weak_owners_, _AO_Acquire) == 0) {    // no need to do this store, because we are about    // to destroy everything.    //__libcpp_atomic_store(&__shared_weak_owners_, -1, _AO_Release);    __on_zero_shared_weak();  } else if (__libcpp_atomic_refcount_decrement(__shared_weak_owners_) == -1)    __on_zero_shared_weak();}

这段 __release_weak() 是 libc++ 里 weak_ptr 生命周期管理的“最终清理逻辑”之一,它的目标一句话概括:

当 weak_ptr 被释放时,如果它是最后一个 weak 引用,则销毁整个 control block。

但它的实现非常“反直觉优化”,关键在于:先偷看(load),再决定要不要做昂贵的原子减法(CAS/RMW)


最终上述接口会调用
__on_zero_shared_weak
这个函数会销毁控制块

相关学习资料