C++的模板(十):shared_ptr的上锁问题

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::template
          rebind<_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 {

      void
      swap(__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,那么它的执行导致_M_refcount被换走。当前一个线程再次恢复执行时,
_M_refcount._M_swap(__other._M_refcount);
这条语句实际做的是另一个other和other的交换。导致的结果是,__shared_ptr中保存的_M_ptr副本和它对应的引用计数扩展类的_M_ptr内容不一致。被交换的_shared_ptr有重合的也可类推。

因为无法控制OS不在那个位置产生调度断点,保护这个调度断点的办法是,断点产生时,对执行有副作用的其他线程肯定不运行。也就是说执行这部分语句需要先获得互斥锁。因为只有一个线程能获得互斥锁,所以其他线程肯定不运行。这样就保护了这个断点。

那么退一步来说,如果__shared_ptr不保存_M_ptr,而是设法克服困难直接从引用计数中保存的那个_M_ptr真本去做指针运算符重载,那么这里只剩第二个语句了,这样是不是不用互斥锁就能解决问题呢?即使只有一个语句,由于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->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;
}


  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值