【读书笔记】当析构函数遇到多线程 ── C++中线程安全的对象回调

陈硕同学的"当析构函数遇到多线程 ── C++ 中线程安全的对象回调"是一篇非常优秀的文章,下面列举文章中的一些知识点和经验总结以便能细细品味、消化。

 

对象构造要做到线程安全,惟一的要求是在构造期间不要泄露 this 指针,即

  • 不要在构造函数中注册任何回调
  • 也不要在构造函数中把 this 传给跨线程的对象
  • 即便在构造函数的最后一行也不行

之所以这样规定,是因为在构造函数执行期间对象还没有完成初始化,如果 this 被泄露 (escape) 给了其他对象(其自身创建的子对象除外),那么别的线程有可能访问这个半成品对象,这会造成难以预料的后果。

   

 

引入另外一层间接性,another layer of indirection,用对象来管理共享资源(如果把 Object 看作资源的话),亦即 handle/body 手法 (idiom)。当然,编写线程安全、高效的引用计数 handle 的难度非凡,作为一名谦卑的程序员,用现成的库就行。

 

虽然我们借 shared_ptr 来实现线程安全的对象释放,但是 shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。 根据文档,shared_ptr 的线程安全级别和内建类型、标准库容器、string 一样,即:

  • 一个 shared_ptr 实体可被多个线程同时读取;
  • 两个的 shared_ptr 实体可以被两个线程同时写入,"析构"算写操作;
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。

请注意,这是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。

 

 

现成的 RAIIResource Acquisition Is Initialization handle。我认为 RAII (资源获取即初始化)是 C++ 语言区别与其他所有编程语言的最重要的手法,一个不懂 RAII C++ 程序员不是一个合格的 C++ 程序员。原来的 C++ 教条是"new delete 要配对,new 了之后要记着 delete",如果使用 RAII,要改成"每一个明确的资源配置动作(例如 new)都应该在单一语句中执行,并在该语句中立刻将配置获得的资源交给 handle 对象(如 shared_ptr),程序中一般不出现 delete"shared_ptr 是管理共享资源的利器,需要注意避免循环引用,通常的做法是 owner 持有指向 A shared_ptrA 持有指向 owner weak_ptr

 

 

StockFactory::get() 把原始指针 this 保存到了 boost::function (6),如果 StockFactory 的生命期比 Stock 短,那么 Stock 析构时去回调 StockFactory::deleteStock 就会 core dump。似乎我们应该祭出惯用的 shared_ptr 大法来解决对象生命期问题,但是 StockFactory::get() 本身是个成员函数,如何获得一个 shared_ptr<StockFactory> 对象呢?

 

有办法,用 enable_shared_from_this。这是一个模板基类,继承它,this 就能变身为 shared_ptr 为了使用 shared_from_this(),要求 StockFactory 对象必须保存在 shared_ptr 里。

 

 

有时候我们需要"如果对象还活着,就调用它的成员函数,否则忽略之"的语意,就像 Observable::notifyObservers() 那样,我称之为"弱回调"。这也是可以实现的,利用 weak_ptr,我们可以把 weak_ptr 绑到 boost::function 里,这样对象的生命期就不会被延长。然后在回调的时候先尝试提升为 shared_ptr,如果提升成功,说明接受回调的对象还健在,那么就执行回调;如果提升失败,就不必劳神了。

 

 

尽管本文通篇在讲如何安全地使用(包括析构)跨线程的对象,但我建议尽量减少使用跨线程的对象,我赞同缙大师说的"用流水线,生产者-消费者,任务队列这些有规律的机制,最低限度地共享数据。这是我所知最好的多线程编程的建议了"

 

 

  • 原始指针暴露给多个线程往往会造成 race condition 或额外的簿记负担;
  • 统一用 shared_ptr/scoped_ptr 来管理对象的生命期,在多线程中尤其重要;
  • shared_ptr 是值语意,当心意外延长对象的生命期。例如 boost::bind 和容器;
  • weak_ptr shared_ptr 的好搭档,可以用作弱回调、对象池等;
  • 认真阅读一遍 boost::shared_ptr 的文档,能学到很多东西: http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm
  • 保持开放心态,留意更好的解决办法,比如 unique_ptr。忘掉已被废弃的 auto_ptr

 

posted @ 2011-01-19 16:25  edwardlost  阅读(1419)  评论(0编辑  收藏  举报