一、不归之路
在c++中,垂悬指针是个非常令人头疼的问题,当我们怡然自得地使用一个指针运筹帷幄的时候,突然指针指向的对象发生析构,而作为指针的使用者并不知情,从而造成严重错误,执行”读“操作时还好些,顶多读取的数据错误的,但要是执行”写“操作的话,很容易造成程序崩溃。和”数组越界“一样,编译器同样为”垂悬指针“开了绿灯,但是正常情况下,使用垂悬指针操作内存,基本上就相当于去和死人搭话,很容易“鬼缠身”,除非你有“通灵”的本事,对析构后的对象了如指掌。
二、能屈能伸
为了避免垂悬指针,一些比较强大的c++库,往往都会实现一个叫做”弱引用“(weak_ptr,weakReference等等)的东西来代替原指针使用,我们可以通过弱引用来判断当前指向的对象是否析构,从而防止垂悬指针。weak_ptr有两个作用,第一是配合shared_ptr解决互相引用死锁的问题,第二就是避免垂悬指针产生错误的操作。
就boost里面的弱引用weak_ptr来说,它依赖shared_ptr,对于堆中创建的对象(使用new关键字创建的)我们可以轻松地使用weak_ptr的lock方法,来判断对象是否已经析构,并转化为强引用操作对象。boost的weak_ptr和shared_ptr设计的非常巧妙,既能实现强弱之间转换,还能保持线程安全,而Juce源码里面的WeakReference是非线程安全的,不能在多线程使用。
我们已经见识到了,shared_ptr的“强权”政策,在“栈”中运筹帷幄,在“堆”中称霸一方。然而它也有软的时候,为什么呢?shared_ptr固然有“宁死不屈”的特性,但是我们作为“造物者”,是可以改变它的,这时充当“演员”角色的shared_ptr只能哪里需要去哪里了。shared_ptr要出演的角色是什么呢?我们接着往下看!!
在堆中创建的对象,生死由我们程序设计者来控制,但是在"栈"中创建的对象,是由作用域来控制的,天时不如地利,因为编译器的所在,对于“栈”中的局部对象的析构我们“无力回天“,我们唯一期望的就是,能够准时收到这个局部对象的死讯。weak_ptr是依赖shared_ptr存在的,而正常情况下shared_ptr确不"受理"栈指针的业务,这时可能感觉现在这两个东西无能为力了。古人云:穷则变,变则通,为了“革命”需要,我们先把shared_ptr自身的deleter“阉割”掉,然后放它潜入类的内部去作“眼线”,这时被“免除职务”的shared_ptr唯一的工作就是通风报信了。
- void UnDelete(void *)
- {
- };
- class O
- {
- public:
- O()
- {
- m_o.reset(this,&UnDelete); //由于是栈中的对象,不能delete,所以要重新赋值一个空删除器
- }
- boost::weak_ptr<O> getWeakPtr()
- {
- return m_o;
- }
- void Go()
- {
- printf("o对象未被析构");
- }
- boost::shared_ptr<O> m_o;
- };
- int main(int argc, char** argv)
- {
- boost::weak_ptr<O> wo;
- {
- O o;
- wo = o.getWeakPtr();
- if (boost::shared_ptr<O> so = wo.lock()) //作用域内
- {
- so->Go();
- }
- }
- if (boost::shared_ptr<O> so = wo.lock()) //作用域外
- {
- so->Go();
- }
- return 0;
- }
上面的代码,在单线程中使用,毫无问题,然而在多线程中使用问题又来了。当weak_ptr使用lock()转为shared_ptr时,可以清楚地知道对象的生死,但是在使用时就无法做到高枕无忧了,因为这时的shared_ptr已经放权了,门子不够硬,罩不住了,对象随时都有可能被其他线程的作用域kill掉,从而导致我们踩到“地雷”。这时笔者在Juce源码里面发现了一对“阴阳锁”,这个东西,对于熟悉Juce源码的朋友想必已经见过(哈哈,“阴阳锁”这个词是我自己根据中国传统文化”易学“,起的名字)
- //阳锁
- template <class LockType>
- class GenericScopedLock
- {
- public:
- inline explicit GenericScopedLock (const LockType& lock) : lock_ (lock) { lock.enter(); }
- inline ~GenericScopedLock() { lock_.exit(); }
- private:
- const LockType& lock_;
- };
- //阴锁
- template <class LockType>
- class GenericScopedUnlock
- {
- public:
- inline explicit GenericScopedUnlock (const LockType& lock) : lock_ (lock) { lock.exit(); }
- inline ~GenericScopedUnlock() { lock_.enter(); }
- private:
- const LockType& lock_;
- };
在此之前,必须知道的一点是,局部对象的析构顺序应该是逆向的,也就是说,最先定义的对象最后析构,而最后定义的对象最先析构。了解完这个,我们看以下代码
- class O
- {
- public:
- O(boost::shared_ptr<SpinLock>& _lock)
- :lock(_lock)
- {
- m_o.reset(this,NoDelete);
- }
- boost::weak_ptr<O> getWeakPtr()
- {
- return boost::weak_ptr<O>(m_o,lock);
- }
- void Go()
- {
- printf("o对象未被析构");
- }
- boost::shared_ptr<SpinLock> lock;
- boost::shared_ptr<O> m_o;
- };
- int main(int argc, char** argv)
- {
- boost::weak_ptr<O> wo;
- {
- boost::shared_ptr<SpinLock> lock= new SpinLock();
- SpinLock::ScopedLockType p(*lock); //阳锁
- O o(lock);
- SpinLock::ScopedUnlockType n(*lock); //阴锁
- wo = o.getWeakPtr();
- wo.getLock().enter();
- if (boost::shared_ptr<O> so = wo.lock()) //作用域内
- {
- so->Go();
- }
- wo.getLock().exit();
- }
- wo.getLock().enter();
- if (boost::shared_ptr<O> so = wo.lock()) //作用域外
- {
- so->Go();
- }
- wo.getLock().exit();
- return 0;
- }
为了能再使用时加锁,要在weak_ptr里加入一个Shared_ptr<SpinLock>锁引用成员,并重写一下构造函数,在构造时将保存在对象内的锁引用传入。然后,再给weak_ptr加入一个获取锁引用的方法,shared_ptr<SpinLock> getLock();返回锁引用即可。这样再使用弱引用时,如果对象发生构造和析构时,就会被锁住,从而达到了栈引用的线程安全的目的。
由于时间关系,这个方法还没有经过大规模测试,可能还有诸多问题,望广大的砖家朋友们前来拍砖。