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

作者: , 共 3173 字

智能指针在现代 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.

类似文章:
编程 » C++, 智能指针
前面已经提到std::shared_ptr有三个缺陷:
编程 » C++, Boost, 智能指针
如果理解了侵入式容器,侵入式智能指针也很容易理解。传统的智能指针std::shared_ptr使用了和数据无关的引用计数,这带来两个问题:
编程 » C++, 智能指针
理论上而言,当 C++提供了std::unique_ptr, C++的程序就不应该出现普通指针了。所有普通指针都可以用std::unique_ptr代替,避免手动删除对象。
编程 » C++, 异步
C++11 的标准异步库至少包含下面内容:
看到网上有片段,提到没有必要自己实现自旋锁,因为标准库的 std::mutex 和现在的自旋锁的实现没有两样。比较好奇,翻了一些资料,试图找到答案。
std::thread是 C++ 11 新引入的标准线程库。在同样是 C++ 11 新引入的 lambda 函数的辅助下,std::thread用起来特别方便:
由 Facebook 开发和维护的 C++库 Folly 提供了自旋锁的实现folly::MicroSpinLock,代码文件地址:https://github.com/facebook/folly/blob/master/folly/synchronization/MicroSpinLock.h
编程 » C++, C++标准库
std::tuple的原理并不复杂,但有些细节非常有意思。其中有一个是至少在gnu C++ std的实现中,std::tuple是倒序存储的:
由 Facebook 开发和维护的 C++库 Folly 提供了锁folly::MicroLock,代码文件地址:https://github.com/facebook/folly/blob/master/folly/MicroLock.h
编程 » C++, 编译错误
在 gcc 中,存在继承关系的模版类,子类无法直接访问父类的成员,即使该成员是protectedpublic
看到网上有片段,提到没有必要自己实现自旋锁,因为标准库的 std::mutex 和现在的自旋锁的实现没有两样。比较好奇,翻了一些资料,试图找到答案。
编程 » C++, Boost, 数据容器
Boost.Instrusive 是一个很有意思的实现,里面实现了很多侵入式容器,在特定环境下,可以大大提升性能。