shared_ptr的线程安全性
shared_ptr的reference count是线程安全的,但是指向的对象不是线程安全的!
本文链接:https://blog.csdn.net/D_Guco/article/details/80155323
shared_ptr的出现在某种程度上解放了c++程序员,c++11标准原生的支持了并发编程,在并发编程中shared_ptr的线程安全问题如何保证呢?先撇开shared_ptr对象的线程安全性,先看shared_ptr本身的线程安全问题。
我们知道,shared_ptr的底层实现原理是引用计数,关于这个计数是否线程安全呢,如果我们把shared_ptr分别传递到不同的线程中,是否会在成引用计数的竞争问题。我们来看shared_ptr引用计数的底层实现。shared_ptr继承了下面的模板类,用它来管理引用计数。其中有两个变量一个表示shared_ptr的引用数,另外一个表示weak_ptr的引用数,我们知道weak_ptr不会增加只能指针的引用数也就是说不持有对象,他的使用必须通过lock方法获取它指向的shared_ptr才能使用。
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
public:
_Sp_counted_base() noexcept
: _M_use_count(1), _M_weak_count(1) { }
virtual
~_Sp_counted_base() noexcept
{ }
//当_M_use_count为0时调用,是个纯虚函数(必须实现),这个函数的作用是释放指针指向的对象所持有的资源,即*this
virtual void
_M_dispose() noexcept = 0;
// 当_M_weak_count为0时调用,释放自己本身的资源,即this
// _M_weak_count = _M_weak_count + (_M_use_count!= 0),当_M_weak_count和_M_use_count都为0时释放this
virtual void
_M_destroy() noexcept
{ delete this; }
virtual void*
_M_get_deleter(const std::type_info&) noexcept = 0;
//增加一个引用
void
_M_add_ref_copy()
{ __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
void
_M_add_ref_lock();
bool
_M_add_ref_lock_nothrow();
void
_M_release() noexcept
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
//首先use_count减去1,并对比减操作之前的值,如果减之前是1,说明减后是0,a1没有任何shared_ptr指针指向它了将销毁对象
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
//如果destory和dispose存在内存屏障,保证dispose函数的效果在destory函数的调用该线程的可见性
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
//同时对a1的weak_count减去1,也对比减操作之前的值,如果减之前是1,说明减后是0,a1没有weak_ptr指向它了,
//应该将管理对象销毁,于是调用_M_destroy()销毁了管理对象
_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();
}
}
}
void
_M_weak_add_ref() noexcept
{ __gnu_cxx::__atomic_add_dispatch(&_M_weak_count, 1); }
void
_M_weak_release() noexcept
{
_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);
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
_M_destroy();
}
}
//获取引用计数
long
_M_get_use_count() const noexcept
{
return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}
private:
_Sp_counted_base(_Sp_counted_base const&) = delete;
_Sp_counted_base& operator=(_Sp_counted_base const&) = delete;
_Atomic_word _M_use_count;
_Atomic_word _M_weak_count;
};
可能有一些人看过boost中智能指针引用计数的实现,不过好像是通过锁来实现的。这里我们可以看到跟boost的是有所不同的,这里的智能指针的引用计数在手段上使用了atomic原子操作,只要在shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。
首先源自操作是线程安全的,所有智能指针在多线程下引用计数也是安全的,也就是说智能指针在多线程下传递使用时引用计数是不会有线程安全问题的,但是这能真正的保证shared_ptr指针的线程安全问题吗。
虽然通过原子操作解决了引用计数的计数的线程安全问题, 但是智能指针指向的对象的线程安全问题,智能指针没有做任何的保证。 首先智能指针有两个变量,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象, 当智能指针发生拷贝的时候,标准哭的实现是县拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子的,隐患就出现在这里,引用一下陈硕老师的例子:地址点击打开链接
这里陈硕老师说道:“这正是多线程读写同一个shared_ptr必须枷锁的原因”, 为了保证程序的绝对的安全是没错的, 但也不是绝对,上面的情景是特殊场景,这种场景也只是为了说明问题而已,真正开发过程中不一定会用到此场景。其实这个问题的根本还是上面说的智能指针指向的对象的线程安全,shared_ptr没有做任何保证,上面的情景就打破了这一准则,在赋值的过程中,改变了shard_ptr指向的对象的内容,甚至不只是修改了对象这么简单,上面的情景直接把智能指针指向的对象给换了。这中情况不用想肯定会出问题。如果你能保证不会有多个线程同时修改或替换指针指向的对象,不用加锁是完全没有问题的,或者说指针指向的对象本身已经是线程安全(包括多线程下的读写安全和构造析构安全)。总之一句话智能指针指向的对象的线程安全,标准库是没有保证的。