设为首页 加入收藏

TOP

1.10 shared_ptr 技术与陷阱
2013-10-07 16:00:41 来源: 作者: 【 】 浏览:62
Tags:1.10 shared_ptr 技术 陷阱

1.10 shared_ptr 技术与陷阱

意外延长对象的生命期shared_ptr 是强引用(“铁丝”绑的),只要有一个指向x 对象的shared_ptr 存在,该对象就不会析构。而shared_ptr 又是允许拷贝构造和赋值的(否则引用计数就无意义了),如果不小心遗留了一个拷贝,那么对象就永世长存了。例如前面提到如果把p. 16 中L48 observers_ 的类型改为vector<shared_ptr<Observer> >,那么除非手动调用unregister(),否则Observer 对象永远不会析构。即便它的析构函数会调用unregister(),但是不去unregister() 就不会调用Observer 的析构函数,这变成了鸡与蛋的问题。这也是Java 内存泄漏的常见原因。

另外一个出错的可能是boost::bind,因为boost::bind 会把实参拷贝一份,如果参数是个shared_ptr,那么对象的生命期就不会短于boost::function 对象:

  1. class Foo  
  2. {  
  3. void doit();  
  4. };  
  5. shared_ptr<Foo> pFoo(new Foo);  
  6. boost::function<void()> func = boost::bind(&Foo::doit, pFoo); // long life foo 

这里func 对象持有了shared_ptr<Foo> 的一份拷贝,有可能会在不经意间延长倒数第二行创建的Foo 对象的生命期。

函数参数因为要修改引用计数(而且拷贝的时候通常要加锁),shared_ptr 的拷贝开销比拷贝原始指针要高,但是需要拷贝的时候并不多。多数情况下它可以以const reference 方式传递,一个线程只需要在最外层函数有一个实体对象,之后都可以用const reference 来使用这个shared_ptr。例如有几个函数都要用到Foo 对象:

  1. void save(const shared_ptr<Foo>& pFoo); // pass by const reference  
  2. void validateAccount(const Foo& foo);  
  3. bool validate(const shared_ptr<Foo>& pFoo) // pass by const reference  
  4. {  
  5. validateAccount(*pFoo);  
  6. // ...  

那么在通常情况下,我们可以传常引用(pass by const reference):
  1. void onMessage(const string& msg)  
  2. {  
  3. shared_ptr<Foo> pFoo(new Foo(msg)); // 只要在最外层持有一个实体,安全不成问题  
  4. if (validate(pFoo)) { // 没有拷贝pFoo  
  5. save(pFoo); // 没有拷贝pFoo  
  6. }  

遵照这个规则,基本上不会遇到反复拷贝shared_ptr 导致的性能问题。另外由于pFoo 是栈上对象,不可能被别的线程看到,那么读取始终是线程安全的。

析构动作在创建时被捕获这是一个非常有用的特性,这意味着:

虚析构不再是必需的。

shared_ptr<void> 可以持有任何对象,而且能安全地释放。

shared_ptr 对象可以安全地跨越模块边界,比如从DLL 里返回,而不会造成从模块A 分配的内存在模块B 里被释放这种错误。

二进制兼容性,即便Foo 对象的大小变了,那么旧的客户代码仍然可以使用新的动态库,而无须重新编译。前提是Foo 的头文件中不出现访问对象的成员的inline 函数,并且Foo 对象的由动态库中的Factory 构造,返回其shared_ptr。

析构动作可以定制。

最后这个特性的实现比较巧妙,因为shared_ptr<T> 只有一个模板参数,而“析构行为”可以是函数指针、仿函数(functor)或者其他什么东西。这是泛型编程(www.cppentry.com)和面向对象编程(www.cppentry.com)的一次完美结合。有兴趣的读者可以参考Scott Meyers 的文章12。这个技术在后面的对象池中还会用到。

析构所在的线程对象的析构是同步的,当最后一个指向x 的shared_ptr 离开其作用域的时候,x 会同时在同一个线程析构。这个线程不一定是对象诞生的线程。

这个特性是把双刃剑:如果对象的析构比较耗时,那么可能会拖慢关键线程的速度(如果最后一个shared_ptr 引发的析构发生在关键线程);同时,我们可以用一个单独的线程来专门做析构,通过一个BlockingQueue<shared_ptr<void> > 把对象的析构都转移到那个专用线程,从而解放关键线程。

现成的RAII handle 我认为RAII (资源获取即初始化)是C++(www.cppentry.com) 语言区别于其他所有编程(www.cppentry.com)语言的最重要的特性,一个不懂RAII 的C++(www.cppentry.com) 程序员不是一个合格的C++(www.cppentry.com) 程序员。初学C++(www.cppentry.com) 的教条是“new 和delete 要配对,new 了之后要记着delete”;如果使用RAII [CCS,条款13],要改成“每一个明确的资源配置动作(例如new)都应该在单一语句中执行,并在该语句中立刻将配置获得的资源交给handle 对象(如shared_ptr),程序中一般不出现delete”。shared_ptr 是管理共享资源的利器,需要注意避免循环引用,通常的做法是owner 持有指向child 的shared_ptr,child 持有指向owner 的weak_ptr。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇1.9 再论shared_ptr 的线程安全 下一篇1.1.1 什么是Boost

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

·MySQL 安装及连接-腾 (2025-12-25 06:20:28)
·MySQL的下载、安装、 (2025-12-25 06:20:26)
·MySQL 中文网:探索 (2025-12-25 06:20:23)
·Shell脚本:Linux Sh (2025-12-25 05:50:11)
·VMware虚拟机安装Lin (2025-12-25 05:50:08)