关于"栈"对象弱引用的线程安全问题
2014-11-10 10:01
316 查看
一、不归之路
在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唯一的工作就是通风报信了。
三、阴阳锁
上面的代码,在单线程中使用,毫无问题,然而在多线程中使用问题又来了。当weak_ptr使用lock()转为shared_ptr时,可以清楚地知道对象的生死,但是在使用时就无法做到高枕无忧了,因为这时的shared_ptr已经放权了,门子不够硬,罩不住了,对象随时都有可能被其他线程的作用域kill掉,从而导致我们踩到“地雷”。这时笔者在Juce源码里面发现了一对“阴阳锁”,这个东西,对于熟悉Juce源码的朋友想必已经见过(哈哈,“阴阳锁”这个词是我自己根据中国传统文化”易学“,起的名字)
在此之前,必须知道的一点是,局部对象的析构顺序应该是逆向的,也就是说,最先定义的对象最后析构,而最后定义的对象最先析构。了解完这个,我们看以下代码
为了能再使用时加锁,要在weak_ptr里加入一个Shared_ptr<SpinLock>锁引用成员,并重写一下构造函数,在构造时将保存在对象内的锁引用传入。然后,再给weak_ptr加入一个获取锁引用的方法,shared_ptr<SpinLock> getLock();返回锁引用即可。这样再使用弱引用时,如果对象发生构造和析构时,就会被锁住,从而达到了栈引用的线程安全的目的。
由于时间关系,这个方法还没有经过大规模测试,可能还有诸多问题,望广大的砖家朋友们前来拍砖。
在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();返回锁引用即可。这样再使用弱引用时,如果对象发生构造和析构时,就会被锁住,从而达到了栈引用的线程安全的目的。
由于时间关系,这个方法还没有经过大规模测试,可能还有诸多问题,望广大的砖家朋友们前来拍砖。
相关文章推荐
- 未将对象引用设置到对象的实例"的问题
- ASP.NET关于"未将对象引用设置到对象的实例"异常的原因
- 关于“ String s = new String( "xyz "); ”创建了几个对象的问题。
- 关于String s1 = new String("abc") 创建一个对象问题和Java常量池总结
- ASP.NET关于"未将对象引用设置到对象的实例"异常的原因
- 关于asp.net 下发送邮件 未能访问"CDO.Message"对象 问题(1)
- 出现"未将对象引用设置到对象的实例“问题的总结
- 单例在多线程下的问题: "懒汉"初始化的线程安全
- 关于String s = new String("xyz"); 创建几个对象的问题
- 关于String a=new String("a")创建几个对象问题的正确答案
- 关于Entity Framework 4中保存时抛出"其它线程在运行,无法新建事务"的问题
- "未将对象引用设置到对象的实例"异常的原因
- C# "未将对象引用设置到对象的实例"异常的原因 总结
- 关于"显示所有文件和文件夹"不能修改的问题
- asp问题之"ActiveX部件不能创建对象 "(2006.7.28)
- 关于ASP.net中的存储过程"为过程或函数指定的参数太多?"的问题
- 关于"建立空文档失败"的问题的分析!
- 关于赋值表达式中出现 "/" 的问题
- 关于"建立空文档失败"的问题的分析
- 关于动态控制 input type="image"对象