一、不归之路
在c++中,垂悬指针是个非常令人头疼的问题,当我们怡然自得地使用一个指针运筹帷幄的时候,突然指针指向的对象发生析构,而作为指针的使用者并不知情,从而造成严重错误,执行”读“操作时还好些,顶多读取的数据错误的,但要是执行”写“操作的话,很容易造成程序崩溃。和”数组越界“一样,编译器同样为”垂悬指针“开了绿灯,但是正常情况下,使用垂悬指针操作内存,基本上就相当于去和死人搭话,很容易“鬼缠身”,除非你有“通灵”的本事,对析构后的对象了如指掌。
二、能屈能伸
为了避免垂悬指针,一些比较强大的c++库,往往都会实现一个叫做”弱引用“(weak_ptr,weakReference等等)的东西来代替原指针使用,我们可以通过弱引用来判断当前指向的对象是否析构,从而防止垂悬指针。weak_ptr有两个作用,第一是配合shared_ptr解决互相引用死锁的问题(在 Juce源码分析(八)强引用与弱引用里面有介绍),第二就是避免垂悬指针产生错误的操作。
就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;
}
在创建对象之前,首先创建一个原子锁(或临界区,根据访问时的代码长短而定),然后保存其强引用;然后使用此引用创建阳锁,阳锁构造时自动加锁。加锁完成后,构造对象,通过被引用类的构造函数将锁引用传入类的成员。最后创建阴锁,阴锁构造,自动解锁。这就相当于,在构造前后分别加入了,enter()和exit()。好了,就这三步就可以了,当对象析构时,阴阳锁会自动锁住对象的析构,其原理是,阴锁最后定义,所以最先释放,于是会执行enter()加锁,然后对象本身析构,阳锁最先定义,所以最后析构,执行exit()解锁。这样,利用阴阳锁,我们不仅锁住了”生“,而且锁住了”死“。
为了能再使用时加锁,要在weak_ptr里加入一个Shared_ptr<SpinLock>锁引用成员,并重写一下构造函数,在构造时将保存在对象内的锁引用传入。然后,再给weak_ptr加入一个获取锁引用的方法,shared_ptr<SpinLock> getLock();返回锁引用即可。这样再使用弱引用时,如果对象发生构造和析构时,就会被锁住,从而达到了栈引用的线程安全的目的。
由于时间关系,这个方法还没有经过大规模测试,可能还有诸多问题,望广大的砖家朋友们前来拍砖。