shared_ptr 引用计数相关问题

前言

智能指针是 C++11 增加的非常重要的特性,并且也是面试的高频考点,本文主要解释以下几个问题:

  • 引用计数是怎么共享的、怎么解决并发问题的
  • 资源释放时,控制块的内存释放吗
  • weak_ptr 怎么判断对象是否已经释放

文中源码用的是 LLVM libcxx-3.5.0,为了方便理解有部分修改,关于自定义删除器和内存池的部分都删掉了。

可以想象 std::shared_ptr 对象在内存中是这样(weak_ptr 内存布局与 shard_ptr 类似):

shared_ptr 控制块

shared_ptr

部分源码如下所示:

template <class _Tp>
class shared_ptr {
 public:
  using element_type = _Tp;
 private:
  element_type*        __ptr_;
  __shared_weak_count* __cntrl_;
 public:
  template<class _Yp>
  shared_ptr(_Yp* __p)
    : __ptr_(__p) {
      __cntrl_ = new __shared_weak_count();
  }

  shared_ptr(const shared_ptr& __r) 
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_) {
    if (__cntrl_ != nullptr) {
      __cntrl_->__add_shared();
    }
  }

  ~shared_ptr() {
    if (__cntrl_ != nullptr) {
       __cntrl_->__release_shared();
    }
  }
};

成员变量

element_type*        __ptr_;
__shared_weak_count* __cntrl_;

可以看到有两个成员变量,一个是指向资源的指针,另一个是指向控制块的指针。

class __shared_count {
 protected:
  long __shared_owners_;
 public:
  __shared_count(long __refs = 0) 
    : __shared_owners_(__refs) {}
};

class __shared_weak_count : private __shared_count {
  long __shared_weak_owners_;
 public:
  __shared_weak_count(long __refs = 0)
    : __shared_count(__refs),
      __shared_weak_owners_(__refs) {}
};

__shared_weak_count 又继承了 __shared_count,它们各有一个成员变量分别记录 shared_ptr 和 weak_ptr 的数量。

需要注意的是计数的初始值是 0,不是 1。

复制构造函数

shared_ptr(const shared_ptr& __r) 
  : __ptr_(__r.__ptr_),
    __cntrl_(__r.__cntrl_) {
  if (__cntrl_ != nullptr) {
    __cntrl_->__add_shared();
  }
}

复制的时候将指针指向同一个资源和同一个控制块,然后增加控制块的计数值,这样就能共享计数值了。那它是怎么保证并发安全的呢?

template <class T>
T increment(T& t) {
  return __sync_add_and_fetch(&t, 1);
}

void __shared_count::__add_shared() {
  increment(__shared_owners_);
}

void __shared_weak_count::__add_shared() {
  __shared_count::__add_shared();
}

它的实现非常简单,就是调用了原子函数将 __shared_owners_ 的值加 1。

可以看到它是通过使用原子函数来解决并发问题的,这样比使用锁的并发性要好一些。

析构函数

~shared_ptr() {
  if (__cntrl_ != nullptr) {
     __cntrl_->__release_shared();
  }
}

析构函数直接调用了一个 __release_shared 函数,它的代码如下:

template <class T>
T decrement(T& t)  {
  return __sync_add_and_fetch(&t, -1);
}

bool __shared_count::__release_shared() {
  if (decrement(__shared_owners_) == -1) {
    __on_zero_shared();
    return true;
  }
  return false;
}

void __shared_weak_count::__release_shared() {
  if (__shared_count::__release_shared()) {
    __release_weak();
  }
}

void __shared_weak_count::__release_weak() {
  if (decrement(__shared_weak_owners_) == -1) {
    __on_zero_shared_weak();
  }
}

在 __shared_weak_count::__release_shared() 内首先调用 __shared_count::__release_shared() 将 __shared_owners_ 的值减 1,如果没有其他 shared_ptr 指向该资源了,就释放资源占用的内存。然后又调用 __shared_weak_count::__release_weak() 将 __shared_weak_owners_ 的值减 1,如果也没有其他 weak_ptr 指向该资源了,就释放控制块占用的资源。

可以看出当最后一个 shared_ptr 析构时,若没有其他 weak_ptr 指向该资源控制块的内存会被释放;否则不会立即释放。那控制块什么时候释放呢?请看下文。

weak_ptr

部分源码如下所示:

template<class _Tp>
class weak_ptr {
 public:
  using element_type = _Tp;
 private:
  element_type*        __ptr_;
  __shared_weak_count* __cntrl_;
public:
  weak_ptr(shared_ptr<_Yp> const& __r)
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_) {
    if (__cntrl_) {
      __cntrl_->__add_weak();
    }
  }

  ~weak_ptr() {
    if (__cntrl_) {
      __cntrl_->__release_weak();
    }
  }

  shared_ptr<_Tp> lock() const {
    shared_ptr<_Tp> __r;
    __r.__cntrl_ = __cntrl_ ? __cntrl_->lock() : __cntrl_;
    if (__r.__cntrl_) {
      __r.__ptr_ = __ptr_;
    }
    return __r;
  }
};

成员变量

element_type*        __ptr_;
__shared_weak_count* __cntrl_;

成员变量与 shared_ptr 一样,也是一个指向资源的指针和一个指向控制块的指针。

构造函数

weak_ptr(shared_ptr<_Yp> const& __r)
  : __ptr_(__r.__ptr_),
    __cntrl_(__r.__cntrl_) {
  if (__cntrl_) {
    __cntrl_->__add_weak();
  }
}

构造函数很简单,就是将指针指向 shared_ptr 所指向的资源和控制块,然后调用 __add_weak 将弱引用计数加 1。

void __shared_weak_count::__add_weak() {
  increment(__shared_weak_owners_);
}

析构函数

~weak_ptr() {
  if (__cntrl_) {
    __cntrl_->__release_weak();
  }
}

析构函数就是调用 __shared_weak_count::__release_weak(),将 __shared_weak_owners_ 的值减 1,当没有其他 shared_ptr & weak_ptr 指向该资源时释放控制块,防止内存泄露。

lock

lock 是 weak_ptr 中很重要的函数,如果我们想使用 weak_ptr 指向的资源,必须先调用 lock() 函数获取一个 shared_ptr。

shared_ptr<_Tp> lock() const {
  shared_ptr<_Tp> __r;
  __r.__cntrl_ = __cntrl_ ? __cntrl_->lock() : __cntrl_;
  if (__r.__cntrl_) {
    __r.__ptr_ = __ptr_;
  }
  return __r;
}

__shared_weak_count* __shared_weak_count::lock()  {
  long object_owners = __shared_owners_;
  while (object_owners != -1) {
    if (__sync_bool_compare_and_swap(&__shared_owners_,
                       object_owners,
                       object_owners+1)) {
      return this;
    }
    object_owners = __shared_owners_;
  }
  return 0;
}

在 weak_ptr::lock() 中首先定义一个了 shared_ptr,然后为它设置指向控制块的指针,如果设置成功再设置指向资源的指针。

__shared_weak_count::lock() 中先获取当前 shared_ptr 的数量,只要指向的资源还存在(__shared_owners_ 不为 -1),就将计数加 1,然后返回控制块指针。

总结

引用计数是怎么共享的,怎么解决并发问题的?

通过使多个 shared_ptr 内部的 __cntrl_ 指向同一个控制块实现计数共享。

使用原子性函数来操作记录计数的变量来解决并发问题。

资源释放时,控制块的内存释放吗?

如果没有其他 weak_ptr 指向该资源,控制块的内存会释放;如果有其他 weak_ptr 指向该资源,那控制块的内存不会释放,由最后一个 weak_ptr 析构时释放。

weak_ptr 怎么判断对象是否已经释放?

使用 weak_ptr 时需要调用 lock() 函数升级成 shared_ptr,此时会检查 __shared_owners_ 看资源是否已释放。

参考资料

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值