@[TOC](从C++20 shared_ptr移除unique()方法浅析多线程同步)
std::shared_ptr
的unique()
方法做了什么事情?
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_object
在b_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_rel
的cas
,而返回引用计数的函数use_count()
使用memory_order_relaxed
atomic load。下面分别分析:
cas(compare and swap)
使用memory_order_acq_rel
,根据memory_order_acq_rel
的语义,可以保证cas
操作之前的内存读写都不能重排到该操作之后,它之后的内存读写都不能重排到它之前. 其他线程中所有对该原子变量的release operation
及其之前的写入都对当前线程该cas操作后可见,并且截止到该cas操作的所有内存写入都对另外线程对它的acquire operation
以及之后的内存操作可见。use_count()
使用memory_order_relaxed
,它只有宽松的内存序,仅保证自身的原子性,不会对任何其他变量的读写产生影响。也不会对其他线程内存同步。
通过以上的分析,我们知道了:
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>
对象都可见use_count()
使用memory_order_relaxed
宽松的内存序,仅保证自身的原子性,也就是说虽然use_count()
访问的是原子变量,但是其并不保证memory order
,并且use_count()
的返回值只能大概反映出当前对象的引用计数,至于为什么,我们在下一节分析。
为什么C++20移除了unique()
方法?
std::shared_ptr
的unique()
方法在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
,导致在多线程场景下,会出现如下状况:
use_count() = 0
并不能说明其他线程对shared_ptr
持有对象的释放已经完成了,相反,由于使用memory_order_relaxed
,有可能其他线程正在释放对象的过程中,只不过调用use_count()
的线程没有看到use_count() = 1
并不能说明当前线程是唯一一个持有该对象的线程,只能说当前线程看到shared_ptr
的引用计数值为1,其他线程可能处在释放智能指针的过程中,也有可能正准备获取该智能指针。前者之所以会出现是因为memory_order_relaxed
不保证跨线程memory order
,而后者是无法通过任何手段避免的,除非加锁。
结论
std::shared_ptr
的unique()
方法在C++20中被移除,主要原因是因为其实现方法并不能实现所代表的语义。移除之后,我们仍可以通过在代码中加入形如use_count() == 1
的逻辑去判断当前线程是唯一持有对象的线程,前提是我们知晓了它背后的缺陷,并根据实际的应用场景加合适的memory fense
或mutex
。
一个unique()
函数,其实可以引伸出无数多线程编程理论和思想,这些思想可能在我们日常业务编码中用不到,可却随着现代编程方法的演进,实打实的影响着每一位软件开发工程师。笔者作为一个小菜鸟,在此也只是阐述自己的理解,如果不对之处欢迎指教!