C++STL中的智能指针shared_ptr以前没用过,它是不是线程安全过去也没关注过。很多说它是不安全的,也有说是安全的。线程安全的问题,简单测试是测不出,到底怎么样,需要直接看代码。
从代码看,shared_ptr是个简单包装,真正的代码从__shared_ptr开始。shared_ptr不过是换了个名字,并从__shared_ptr引出构造函数和赋值运算:
template<typename _Tp>class shared_ptr: public __shared_ptr<_Tp>{};
__shared_ptr含有2个数据元素:
template<typename _Tp>class __shared_ptr {_Tp* _M_ptr; __shared_count<_Lp> _M_refcount; };
_M_ptr是被__shared_ptr管理的对象指针,_M_refcount又是一个简单包装,内部包含一个指向_Sp_counted_base的指针:
template<_Lock_policy _Lp = __default_lock_policy>class __shared_count{_Sp_counted_base<_Lp>* _M_pi;};
这是个多态指针,实际使用时还要扩展一个指针成员:
template<typename _Ptr, _Lock_policy _Lp>class _Sp_counted_ptr: public _Sp_counted_base<_Lp>{_Ptr _M_ptr; // .... ........... .... ... .....};
这个_M_ptr也是那个被引用计数管理的对象指针,在_Sp_counted_base及其派生类的管理范围内,如果引用计数归0,最后要销毁这个被管理的对象。但回溯到__shared_ptr 类去找到这个_M_ptr,增加了难度,所以这里又保存了一次。反过来看,这里保存的是真本,而__shared_ptr 类保存的是为了优化指针运算符重载而保留的副本。
基础类_Sp_counted_base除了扩展一个指针,根据需要,还可以扩展自定义分配器和清除器。并非所有的对象直接从new出来,有的通过自定义内存管理分配,它们会有自己专用的分配器和清除器:
template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>class _Sp_counted_deleter: public _Sp_counted_ptr<_Ptr, _Lp>{typedef typename _Alloc::templaterebind<_Sp_counted_deleter>::other _My_alloc_type;struct _My_Deleter: public _My_alloc_type {_Deleter _M_del; _My_Deleter(_Deleter __d, const _Alloc& __a): _My_alloc_type(__a), _M_del(__d) { }};protected:_My_Deleter _M_del; // .... ........... .... ... .....};
而引用技术基础类_Sp_counted_base又继承了 _Mutex_base进行互斥访问管理:
template<_Lock_policy _Lp>class _Mutex_base{protected:enum { _S_need_barriers = 0 };};template<>class _Mutex_base<_S_mutex>: public __gnu_cxx::__mutex{protected:enum { _S_need_barriers = 1 };};template<_Lock_policy _Lp = __default_lock_policy>class _Sp_counted_base: public _Mutex_base<_Lp>{_Atomic_word _M_use_count; _Atomic_word _M_weak_count; };
从这个数据结构看,STL库中的shared_ptr保存了被管理对象的指针的2个副本。所以使用时要注意保护好一致性。而shared_ptr内置的mutex管理,表明,shared_ptr设计的目标确实是想支持多线程应用程序。
那么这个设计目标到底实现了没有?遗憾的是没有。问题出在swap上:
template<typename _Tp>class __shared_ptr {voidswap(__shared_ptr<_Tp, _Lp>&& __other) {std::swap(_M_ptr, __other._M_ptr);_M_refcount._M_swap(__other._M_refcount);}_Tp* _M_ptr; __shared_count<_Lp> _M_refcount; };
这个代码看是简单,却留下了隐患。因为OS在运行过程中,可以在一个进程或线程的任意位置产生一个调度断点,然后又去调用别的进程或线程的来跑。上面的代码如果在第一条
std::swap(_M_ptr, __other._M_ptr);
执行完立即产生一个调度断点,那么别的线程可以在这个点上调度运行,如果凑巧也执行了一个 __shared_ptr ::swap(),并且__shared_ptr this指针也是那个__shared_ptr,那么它的执行导致_M_refcount被换走。
当前一个线程再次恢复执行时,
_M_refcount._M_swap(__other._M_refcount);
实际做的是另一个other和other的交换。导致的结果是,__shared_ptr中保存的_M_ptr副本和引用技术ptr扩展类的副本内容不一致。
因为无法控制OS不在那个位置产生调度断点,保护这个调度断点的办法是,断点产生时,对执行有副作用的其他线程肯定不运行。也就是说执行这部分语句需要先获得互斥锁。因为只有一个线程能获得互斥锁,所以其他线程肯定不运行。这样就保护了这个断点。
那么退一步来说,如果__shared_ptr不保存_M_ptr,而是设法克服困难直接从引用计数中保存的那个_M_ptr真本去做指针运算符重载,那么这里只剩地2个语句了,这样是不是不用互斥锁就能解决问题呢?即使只有一个语句,由于swap这个操作,交换过程中会产生tmp副本,这样仍然不安全。
所以就是要加锁。加锁的办法,如果管理的是少量的大粒度的对象,可以使用单一的全局锁,如果管理的是大量的细粒度的对象,就要使用局部锁。对象粒度的锁。此外细粒度的锁还要小心的管理上锁顺序,防止出现死锁。
void swap(shared_ptr<T> &ptr){Lock2 lock(*this, ptr);shared_ptr<T>::swap(ptr);}
上锁在锁对象的构造函数执行lock,析构函数中执行unlock,这是为了防止上锁区域的语句抛出异常,如果不是析构函数,unlock就可能执行不到。
注意shared_ptr构造函数中也用到了swap,其中作为局部变量的临时的shared_ptr,引用计数肯定是1,或者根本就没有引用计数,可以不上锁。
最后就是修改shared_ptr的源代码来解决这个问题。如果只是测试想法,也可以不立即修改shared_ptr的代码,而是遇到swap时,通过强制类型转换执行上锁的代码。
#include <cstdio>
#include <memory>
using namespace std;struct A {static int next;int a;A() { a=next++; printf("create A:%d\n", a);}~A() { printf("destroy A:%d\n", a);}
};
int A::next=1000;template <class T, __gnu_cxx::_Lock_policy LP=__default_lock_policy>
struct swaplock :shared_ptr<T> {typedef _Sp_counted_base<LP> counted_base;
public:struct B {T *pa;struct C: counted_base {T *_M_ptr;} *p;};struct Lock2 {bool lock1;bool lock2;__gnu_cxx::__mutex *m1;__gnu_cxx::__mutex *m2;Lock2(shared_ptr<T> &p1, shared_ptr<T> &p2){B *p= (B*)&p1;B *q= (B*)&p2;lock1=lock2=false;if (p==q) return;if (p>q) std::swap(p, q);m1= p->p;m2= q->p;if(m1 && p->p->_M_get_use_count()>1) {m1->lock();lock1 = true;}if(m2 && q->p->_M_get_use_count()>1) {m2->lock();lock2 = true;}}~Lock2(){if(lock2) m2->unlock();if(lock1) m1->unlock();}};void swap(shared_ptr<T> &ptr){Lock2 lock(*this, ptr);shared_ptr<T>::swap(ptr);}void reset() { *(shared_ptr<T>*)this = shared_ptr<T>(); }bool corrupt() {B *q= (B*)this; return q->pa!=q->p->_M_ptr;}
};int main()
{shared_ptr<A> p(new A);shared_ptr<A> p2(new A);printf("1:%d, 2:%d\n", p->a, p2->a);((swaplock<A>&)p).swap(p2);printf("1:%d, 2:%d\n", p->a, p2->a);if( ((swaplock<A>&)p).corrupt()) {printf("error1\n");}if( ((swaplock<A>&)p2).corrupt()) {printf("error2\n");}p=p2;printf("1:%d, 2:%d\n", p->a, p2->a);return 0;
}