6

标准库的 std::shared_ptr 是如何实现的?

 3 years ago
source link: https://zhiqiang.org/coding/std-shared-ptr.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

标准库的 std::shared_ptr 是如何实现的?

作者: 张志强

, 发表于 2020-01-09

, 共 3173 字 , 共阅读 683 次

智能指针在现代 C++里用得越多。以前只知道它大致的原理,比如使用引用计数。但很多实现细节并不清楚,最直接的,它是如何实现多线程安全的?今天找了 gnu c++ library 的实现好好看了一下。

std::shared_ptr的代码位于https://gcc.gnu.org/onlinedocs/gcc-7.5.0/libstdc++/api/a15815_source.html,它基本等价于:

template <typename T>
using std::shared_ptr = std::__shared_ptr<T>;

因此我们需要去看std::__shared_ptr<T>的实现,后者位于https://gcc.gnu.org/onlinedocs/gcc-7.5.0/libstdc++/api/a00494_source.html,几乎所有的定义都位于该文件。其继承和内存结构如下:

template<typename _Tp, _Lock_policy _Lp = __default_lock_policy>
class __shared_ptr : __shared_ptr_access<_Tp, _Lp> {
    _Tp*        _M_ptr;                 // Contained pointer.
    __shared_count<_Lp>  _M_refcount;   // Reference counter.
};

这里_Lock_policy是一个枚举量,它有三个取值

  • _S_single:表示为单线程适用。
  • _S_mutex:表示为多线程使用,并且使用thread-layer abstractions
  • _S_atomic:多线程使用,使用原子操作。

__default_lock_policy在多线程程序中一般为_S_atomic(实际值依赖于编译设置,具体定义参见https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01123.html)。

其中基类__shared_ptr_access主要是为智能指针提供*以及->等运算符,可以忽略。

其内存布局中,第一个是指针本身,这个没什么好说的。最重要的是第二个成员__shared_count,请定义如下:

template<_Lock_policy _Lp>
class __shared_count {
    _Sp_counted_base<_Lp>*  _M_pi;
public:
    __shared_count& operator=(const __shared_count& __r) noexcept {
        _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        if (__tmp != _M_pi) {
            if (__tmp != 0) __tmp->_M_add_ref_copy();
            if (_M_pi != 0) _M_pi->_M_release();
            _M_pi = __tmp;
        }
        return *this;
    }
};

这里面保存了一个了引用计数器的指针,也就是说std::shared_ptr实际布局中为两个指针,共使用 16 字节。

先看其基类本身的定义如下,它保存了两个引用计数:

  • _M_use_count:引用次数(std::shared_ptr指向次数)。当为 0 时,共享指针保存的元素将被删除。
  • _M_weak_count:弱引用次数(std::weak_ptr指向次数),如果引用次数大于 0 ,再加 1。当为 0 时,当前计数器将被删除。
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp> {
    _Atomic_word  _M_use_count;     // #shared
    _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)

public:
    void _M_release() noexcept {
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) {
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
            _M_dispose();

            if (_Mutex_base<_Lp>::_S_need_barriers) {
                __atomic_thread_fence (__ATOMIC_ACQ_REL);
            }

            _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
            if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) {
                _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
                _M_destroy();
            }
        }
    }

    // Called when _M_weak_count drops to zero.
    virtual void _M_destroy() noexcept { delete this; }

    // Called when _M_use_count drops to zero, to release the resources
    // managed by *this.
    virtual void _M_dispose() noexcept = 0;

    void _M_add_ref_copy() { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
}

其中_M_destroy_M_dispose都在继承类中被重新定义。不过实现中很容易看出为什么引用计数可以做到线程安全,它使用了atomic原子数据,以及很多memory barrier

但实际引用计数指向的是继承的类,主要是_Sp_counted_deleter_Sp_counted_ptr

【有待完善】

Q. E. D.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK