从C++20 shared_ptr移除unique()方法浅析多线程同步

@[TOC](从C++20 shared_ptr移除unique()方法浅析多线程同步)

题图

std::shared_ptrunique()方法做了什么事情?

unique()作为std::shared_ptr的成员函数,它检查当前shared_ptr持有的对象,是不是该对象的唯一持有者。也就是说检查shard_ptr的引用计数是否为1。大概的实现如下

bool unique() {
    return this->use_count() == 1;
}

工程上我曾用它来管控对象的所有权。如:多个对象同时通过std::shared_ptr持有a_object,但是我希望b_object是最后一个持有a_object的对象,也就是说希望a_objectb_object释放,于是,在b_object中设计如下函数:

class a_object;
class b_object {
void check_unique() {
    if (a_holder_.unique()) {
        a_holder_.reset();
    } else {
        // 一段时间之后再执行check_unique()...
    }
}
shared_ptr<a_object> a_holder_;
};

shared_ptr()引用计数通过什么保证线程安全?

根据gcc 10.02源码,shared_ptr引用计数的类型是_Atomic_word原子变量,增加引用计数使用memory_order_acq_relcas,而返回引用计数的函数use_count()使用memory_order_relaxed atomic load。下面分别分析:

  1. cas(compare and swap)使用memory_order_acq_rel,根据memory_order_acq_rel的语义,可以保证cas操作之前的内存读写都不能重排到该操作之后,它之后的内存读写都不能重排到它之前. 其他线程中所有对该原子变量的release operation及其之前的写入都对当前线程该cas操作后可见,并且截止到该cas操作的所有内存写入都对另外线程对它的acquire operation以及之后的内存操作可见。
  2. use_count()使用memory_order_relaxed,它只有宽松的内存序,仅保证自身的原子性,不会对任何其他变量的读写产生影响。也不会对其他线程内存同步。

通过以上的分析,我们知道了:

  1. shared_ptr增加引用计数的操作,也就是在shared_ptr获取对象的过程中,其可以和其他线程形成happens-before关系,从而建立某些变量间的memory order。由于我们一般使用shared_ptr<T> sp(new T)shared_ptr<T> sp(make_shared<T>)方式创建动态指针,因此如果两个shared_ptr<T>建立了happens-before关系,可以保证shared_ptr持有的对象对两个shared_ptr<T>对象都可见
  2. use_count()使用memory_order_relaxed宽松的内存序,仅保证自身的原子性,也就是说虽然use_count()访问的是原子变量,但是其并不保证memory order,并且use_count()的返回值只能大概反映出当前对象的引用计数,至于为什么,我们在下一节分析。

为什么C++20移除了unique()方法?

std::shared_ptrunique()方法在C++17中被废弃,在C++20中被移除。在cpp reference中有如下说明:

This function was deprecated in C++17 and removed in C++20 because use_count is only an approximation in multithreaded environment (see Notes in use_count)

可以理解,unique()本质上通过调用use_count()判断引用计数是否为1,而use_count()并不能提供unique()所代表的语义:当前shared_ptr是否为持有对象的唯一持有者。cppreference中std::shared_ptr<T>::use_count()解释的更为详细:

comparison with ​0​. If use_count returns zero, the shared pointer is empty and manages no objects (whether or not its stored pointer is null). In multithreaded environment, this does not imply that the destructor of the managed object has completed.

comparison with 1. If use_count returns 1, there are no other owners. (The deprecated member function unique() is provided for this use case.) In multithreaded environment, this does not imply that the object is safe to modify because accesses to the managed object by former shared owners may not have completed, and because new shared owners may be introduced concurrently, such as by std::weak_ptr::lock.

也就是说,由于use_count()大多数的实现方法(比如我使用的gcc)都使用memory_order_relaxed,导致在多线程场景下,会出现如下状况:

  1. use_count() = 0并不能说明其他线程对shared_ptr持有对象的释放已经完成了,相反,由于使用memory_order_relaxed,有可能其他线程正在释放对象的过程中,只不过调用use_count()的线程没有看到
  2. use_count() = 1并不能说明当前线程是唯一一个持有该对象的线程,只能说当前线程看到shared_ptr的引用计数值为1,其他线程可能处在释放智能指针的过程中,也有可能正准备获取该智能指针。前者之所以会出现是因为memory_order_relaxed不保证跨线程memory order,而后者是无法通过任何手段避免的,除非加锁。

结论

std::shared_ptrunique()方法在C++20中被移除,主要原因是因为其实现方法并不能实现所代表的语义。移除之后,我们仍可以通过在代码中加入形如use_count() == 1的逻辑去判断当前线程是唯一持有对象的线程,前提是我们知晓了它背后的缺陷,并根据实际的应用场景加合适的memory fensemutex

一个unique()函数,其实可以引伸出无数多线程编程理论和思想,这些思想可能在我们日常业务编码中用不到,可却随着现代编程方法的演进,实打实的影响着每一位软件开发工程师。笔者作为一个小菜鸟,在此也只是阐述自己的理解,如果不对之处欢迎指教!

参考链接

  1. C++标准库(五)之智能指针源码剖析
  2. C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值