Chinaunix首页 | 论坛 | 博客
  • 博客访问: 87790
  • 博文数量: 44
  • 博客积分: 2525
  • 博客等级: 少校
  • 技术积分: 316
  • 用 户 组: 普通用户
  • 注册时间: 2010-04-25 17:01
文章分类

全部博文(44)

文章存档

2010年(44)

我的朋友

分类:

2010-04-27 10:47:59

一些启发

 

指向对象的原始指针 (raw pointer) 是坏的,尤其当暴露给别的线程时。Observable 应当保存的不是原始的 Observer*,而是别的什么东西,能分别 Observer 对象是否存活。类似地,如果 Observer 要在析构函数里解注册(这虽然不能解决前面提到的 race condition,但是在析构函数里打扫战场还是应该的),那么 subject_ 的类型也不能是原始的 Observable*

 

有经验的 C++ 程 序员或许会想到用智能指针,没错,这是正道,但也没那么简单,有些关窍需要注意。这两处直接使用 shared_ptr 是不行的,会造成循环引用,导致资源泄漏。别着急,后文会一一讲到。

 

图片请看 PDF 版,目前 CSDN 博客的上传图片功能失灵了。


原始指针有何不妥?

 

有两个指针 p1 和 p2, 指向堆上的同一个对象 Objectp1 和 p2 位于不同的线程中(左图)。假设线程 透 过 p1 指针将对象销毁了(尽管把 p1 置为了 NULL), 那么 p2 就成了空悬指针(右图)。

     

要想安全地销毁对象,最好让在别人(线程)都看不到的情 况下,偷偷地做。


一个“解决办法”

 

一个解决空悬指针的办法是,引入一层间接性,让 p1 和 p2 所 指的对象永久有效。比如下图的 proxy 对象,这个对象,持有一个指向 Object 的指针。(从 语言的角度,p1 和 p2 都 是二级指针。)

        

当销毁 Object 之后,proxy 对象继续存在,其值变为 0。而 p2 也 没有变成空悬指针,它可以通过查看 proxy 的内容来判断 Object 是否还活着。要线程安全地释放 Object 也不是那么容易,race condition 依旧存在。比如 p2 看第一眼的时候 proxy 不是零,正准备去调用 Object 的成员函数,期间对象已经被 p1 销毁了。

 

问题在于,何时释放 proxy 指针呢?


一个更好的解决办法

 

为了安全地释放 proxy, 我们可以引入引用计数,再把 p1 和 p2 都从指针变成对象 sp1 和 sp2proxy 现在有两个成员,指针和计数器。

1. 一开始,有两个引用,计数值为 2

2. sp1 析构了,引用计数的值减为 1

3. sp2 也析构了,引用计数的值为 0,可以安全地销毁 proxy 和 Object 了。

打住!这不就是引用计数型智能指针吗?


一个万能的解决方案

 

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

 

万幸,C++ 的 tr1 标 准库里提供了一对神兵利器,可助我们完美解决这个头疼的问题。


神器 shared_ptr/weak_ptr

shared_ptr 是引用计数型智能指针,在 boost 和 std::tr1 里都有提供,现代主流的 C++ 编译器都能很好地支持。shared_ptr 是一个类 模板 (class template),它只有一个类型参数,使用起来很方便。引用计数的是自动化资源管理的常用手法,当引用计数降为 时, 对象(资源)即被销毁。weak_ptr 也是一个引用计数型智能指针,但是它不增加引用次数,即弱 (weak) 引用。

shared_ptr 的基本用法和语意请参考手册或教程,本文从略,这里谈几个关键点。

 

shared_ptr 控制对象 的生命期。shared_ptr 是强引用(想象成用铁丝绑住堆上的对象),只要有一个指向 对象 的 shared_ptr 存在,该 对象就不会析构。当指向对象 的最后一个 shared_ptr 析构或 reset 的时候,保证会被销毁。

weak_ptr 不控制对象的 生命期,但是它知道对象是否还活着(想象成用棉线轻轻拴住堆上的对象)。如果对象还活着,那么它可以提升 (promote) 为有效的 shared_ptr;如果对象已经死了,提升会失败,返回一个空的 shared_ptr

shared_ptr/weak_ptr 的“计数”在主流平台上是原子操作,没有用锁,性能不俗。

shared_ptr/weak_ptr 的线程安全级别与 string 等 STL 容器一样,后面还会讲。


插曲:系统地避免各种指针错误

 

我同意孟岩说的“大部分用 写的 上规模的软件都存在一些内存方面的错误,需要花费大量的精力和时间把产品稳定下来。”内存方面的问题在 C++ 里 很容易解决,我第一次也是最后一次见到别人的代码里有内存泄漏是在 2004 年实习那会儿,自己写的C++ 程 序从来没有出现过内存方面的问题。

 

C++ 里可能出现的内存问题大致有这么几个方面:

 

1. 缓冲区溢出

2. 空悬指针/野指针

3. 重复释放

4. 内存泄漏

5. 不配对的 new[]/delete

6. 内存碎片

 

正确使用智能指针能很轻易地解决前面 个问 题,解决第 个问题需要别的思路,我会另文探讨。

 

1. 缓冲区溢出  用 vector/string 或自己编写 Buffer 类来管理缓冲区,自动记住用缓冲区的 长度,并通过成员函数而不是裸指针来修改缓冲区。

2. 空悬指针/野指 针  用 shared_ptr/weak_ptr,这正是本文的主题

3. 重复释放  用 scoped_ptr,只在对象析构的时候释放一次

4. 内存泄漏  用 scoped_ptr,对象析构的时候自动释放内存

5. 不配对的 new[]/delete  把 new[] 统统替换为 vector/scoped_array

 

正确使用上面提到的这几种智能指针并不难,其难度大概比 学习使用 vector/list 这些标准库组件还要小,与 string 差不多,只要花一周的时间去适应它, 就能信手拈来。我觉得,在现代的 C++ 程序中一般不会出现 delete 语句,资源(包括复杂对象本身)都是 通过对象(智能指针或容器)来管理的,不需要程序员还为此操心。

 

需要注意一点:scoped_ptr/shared_ptr/weak_ptr 都是值语意,要么是栈上对象,或是其他对象的直接数据成员。几乎不会有下面这种用法:

 shared_ptr* pFoo = new shared_ptr(new Foo);  // WRONG semantic

 

还要注意,如果这几种智能指针是对象 的数 据成员,而它的模板参数 是个 incomplete 类型,那么 的析构函数不能是默认的或内联的,必须在 .cpp 文件里边显式定义,否则会有编译错或运行错。


应用到 Observer 

 

既然透过 weak_ptr 能探查对象的生死,那么 Observer 模式的竞态条件就很容易解决,只要让 Observable 保存 weak_ptr 即可:

 

class Observable  // not 100% thread safe!

{

 public:

  void register(weak_ptr x);

  void unregister(weak_ptr x);  // 可用 std::remove/vector::erase 实现

 

  void notifyObservers()

  {

    MutexLock lock(mutex_);

    Iterator it = observers_.begin();

    while (it != observers_.end()) {

      shared_ptr obj(it->lock());  // 尝试提升,这一步是线程安全的

      if (obj) {

        // 提升成功,现在引用计数值至少为 (想 想为什么?)

        obj->update();  // 没有竞态条件,因为 obj 在栈上,对象不可能在本作用域内销毁

        ++it;

      } else {

        // 对象已经销毁,从容器中拿掉 weak_ptr

        it = observers_.erase(it);

      }

    }

  }

 

 private:

  std::vector<weak_ptr > observers_;  // (5)

  mutable Mutex mutex_;

};

 

就这么简单。前文代码 (3) 处的竞态条件已经弥补了。


解决了吗?

 

把 Observer* 替换为 weak_ptr 部分解决了 Observer 模式的线程安全,但还有几个疑 点:

 

不灵活,强制要求 Observer 必须以 shared_ptr 来管理;

 

不是完全线程安 全Observer 的析构函数会调用 subject_->unregister(this),万 一 subject_ 已经不复存在了呢?为了解决它,又要求 Observable 本身是用 shared_ptr 管理的,并且 subject_ 是个 weak_ptr

 

线程瓶颈 (thread contention),即 Observable 的三个成员函数都用了互斥器来同步,这会造成 register 和 unregister 等待 notifyObservers,而后者的执行时间是无上限的,因为它同步回调了用户提供的 update() 函数。我们希望 register 和 unregister 的执行时间不会超过某个固定的值,以免即便殃及无辜群众。

 

死锁,万一 update() 虚函数中调用了 (un)register 呢?如果 mutex_ 是不可重入的,那么会死锁;如果 mutex_ 是可重入的,程序会面临迭代器失效(core dump 是最好的结果),因为 vector observers_ 在遍历期间被 无意识地修改了。这个问题乍看起来似乎没有解决办法,除非在文档里做要求。

 

这些问题留到本文附录中去探讨,每个问题都是能解决的。

 

我个人倾向于使用不可重入的 Mutex, 例如 pthreads 默认提供的那个,因为“要求 Mutex 可重入”本身往往以为着设计上出了问 题。Java 的 intrinsic  lock 是可重入的,因为要允许 synchronized 方法相互调用,我觉得这 也是无奈之举。

思考:如果把 (5) 处 改为 vector > observers_;,会有什么后果?


再论 shared_ptr 的线程安全

 

虽然我们借 shared_ptr 来实现线程安全的对象释放,但是 shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。

 

根据文档shared_ptr 的线程安全级别和内建类 型、标准库容器、string 一样,即:

 

一个 shared_ptr 实体可被多个线程同时读取;

两个的 shared_ptr 实体可以被两个线程同时写入,“析构”算写操作;

如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。

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

 

要在多个线程中同时访问同一个 shared_ptr,正确的做法是:

 

shared_ptr globalPtr;

Mutex mutex; // No need for ReaderWriterLock

void doit(const shared_ptr& pFoo);

 

globalPtr 能被多个线程看到,那么它的读写需要加锁。注意我们不必用读写锁,而只用最简单的互斥锁,这是为了性 能考虑,因为临界区非常小,用互斥锁也不会阻塞并发读。

 

void read()

{

  shared_ptr ptr;

  {

    MutexLock lock(mutex);

    ptr = globalPtr;  // read globalPtr

  }

 

  // use ptr since here

  doit(ptr);

}

 

写入的时候也要加锁:

 

void write()

{

  shared_ptr newptr(new Foo);

  {

    MutexLock lock(mutex);

    globalPtr = newptr;  // write to globalPtr

  } 

 

  // use newptr since here

  doit(newptr);

}

 

注意到 read() 和 write() 在临界区之外都没有再访问 globalPtr,而是用了一个指向同一对象的栈 上 local copy。下面会谈到,只要有这样的 local copy 存在,shared_ptr 作为函数参数传递时不必复制,用 reference to const 即可。


10 shared_ptr 技术与陷阱

 

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

 

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

 

class Foo

{

  void doit();

};

 

boost::function func;

shared_ptr pFoo(new Foo);

func = bind(&Foo::doit, pFoo);  // long life foo

 

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

 

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

 

void save(const shared_ptr& pFoo);

void validateAccount(const Foo& foo);

bool validate(const shared_ptr& pFoo)

{

  // ...

  validateAccount(*pFoo);

  // ...

}

 

那么在通常情况下,

 

void onMessage(const string& buf)

{

  shared_ptr pFoo(new Foo(buf));  // 只要在最外层持有一个实体,安全不成问题

  if (validate(pFoo)) {

    save(pFoo);

  }

}

 

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

 

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

 

虚析构不再是必须的。

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

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

二进制兼容性,即便 Foo 对 象的大小变了,那么旧的客户代码任然以使用新的动态库,而无需重新编译(这要求 Foo 的头文件中不出现访问对象的成员的 inline函数)。

析构动作可以定制。

 

这个特性的实现比较巧妙,因为 shared_ptr 只有一个模板参数,而“析构行为”可以是函数指针,仿函数 (functor) 或者其他什么东西。这是泛型编程和面向对象编程的一次完美结合。有兴趣的同学可以参考 

 

这个技术在后面的对象池中还会用到。

 

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

 

现成的 RAII handle。我认为 RAII (资 源获取即初始化)是 C++ 语言区别与其他所有编程语言的最重要的手法,一个不懂 RAII 的 C++ 程 序员不是一个合格的 C++ 程序员。shared_ptr 是管理共享资源的利器,需要注意避免循环引用,通常的做法是 owner 持有指向 的 shared_ptr持有指向 owner 的 weak_ptr


对象池

 

假设有 Stock 类,代表一只股票的价格。每一只股票有一个惟一的字符串标识,比如 Google 的 key 是 "NASDAQ:GOOG"IBM 是 "NYSE:IBM"Stock 对象是个主动对象,它能不断获取新价格。为了节省系统资源,同一个程序里边每一只出现的股票只有一 个 Stock 对象,如果多处用到同一只股票,那么 Stock 对象应该被共享。如果某一只股票没有再在任何地方用到,其对应的 Stock 对象应该析构,以释放资源,这隐含了 “引用计数”。

 

为了达到上述要求,我们可以设计一个对象池 StockFactory。它的接口很简单,根据 key 返回 Stock 对象。我们已经知道,在多线程程序中,既然对象可能被销毁,那么返回 shared_ptr 是合理的。

 

自然地,我们写出如下代码。(可惜是错的)

 

class StockFactory : boost::noncopyable

{  // questionable code

 public:

  shared_ptr get(const string& key);

 

 private:

  std::map > stocks_;

  mutable Mutex mutex_;

};

 

get() 的逻辑很简单,如果在 stocks_ 里找到了 key, 就返回 stocks_[key];否则新建一个 Stock,并存入 stocks_[key]

细心的读者或许已经发现这里有一个问题,Stock 对象永远不会被销毁,因为 map 里存的是 shared_ptr,始终有铁丝绑着。那么或许应 该仿照前面 Observable 那样存一个 weak_ptr?比如

 

class StockFactory : boost::noncopyable

{

 public:

  shared_ptr get(const string& key)

  {

    shared_ptr pStock;

    MutexLock lock(mutex_);

    weak_ptr& wkStock = stocks_[key];  // 如果 key 不存在,会默认构造一个

    pStock = wkStock.lock();  // 尝试把棉线提升为铁丝

    if (!pStock) {

      pStock.reset(new Stock(key));

      wkStock = pStock;  // 这里更新了 stocks_[key],注意 wkStock 是个引用

    }

    return pStock;

  }

 

 private:

  std::map > stocks_;

  mutable Mutex mutex_;

};


这么做固然 Stock 对象是销毁了,但是程序里却出现了轻微的内存泄漏,为什么?

 

因为 stocks_ 的大小只增不减,stocks_.size() 是曾经存活过的 Stock 对象的总数,即便活的 Stock 对象数目降为 0。或许有人认为这不算泄漏,因为内存并不是彻底遗失不能访问了,而是被某个标准库容器占用了。我认为 这也算内存泄漏,毕竟是战场没有打扫干净。

 

其实,考虑到世界上的股票数目是有限的,这个内存不会一 直泄漏下去,大不了把每只股票的对象都创建一遍,估计泄漏的内存也只有几兆。如果这是一个其他类型的对象池,对象的 key 的 集合不是封闭的,内存会一直泄漏下去。

 

解决的办法是,利用 shared_ptr 的定制析构功能。shared_ptr 的构造函数可以有一个额外的 模板类型参数,传入一个函数指针或仿函数 d,在析构对象时执行 d(p)shared_ptr 这么设计并不是多余的,因为反正要在创建对象时捕获释放动作,始终需要一个 bridge

 

template shared_ptr::shared_ptr(Y* p, D d); 

template void shared_ptr::reset(Y* p, D d);

 

那么我们可以利用这一点,在析构 Stock 对象的同时清理 stocks_

 

class StockFactory : boost::noncopyable

{

  // in get(), change 

  // pStock.reset(new Stock(key));

  // to

  // pStock.reset(new Stock(key),

  //               boost::bind(&StockFactory::deleteStock, this, _1));  (6)

 

 private:

  void deleteStock(Stock* stock)

  {

    if (stock) {

      MutexLock lock(mutex_);

      stocks_.erase(stock->key());

    }

    delete stock;  // sorry, I lied

  }

  // assuming FooCache lives longer than all Foo's ...

  // ...

 

这里我们向 shared_ptr::reset() 传递了第二个参数,一个 boost::function,让它在析构 Stock* p 时调用本 StockFactory 对象的 deleteStock 成员函数。

 

警惕的读者可能已经发现问题,那就是我们把一个原始的 StockFactory this 指针保存在了 boost::function 里 (6), 这会有线程安全问题。如果这个 StockFactory 先于 Stock 对象析构,那么会 core dump。正如 Observer 在析构函数里去调用 Observable::unregister(),而那时 Observable 对象可能已经不存在了。

 

当然这也是能解决的,用到下一节的技术。


enable_shared_from_this

 

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

 

有办法,用 enable_shared_from_this。这是一个模板基类,继承它,this 就能变身为 shared_ptr

class StockFactory : public boost::enable_shared_from_this,

                       boost::noncopyable

{ /* ... */ };

 

为了使用 shared_from_this(),要求 StockFactory 对象必须保存在 shared_ptr 里。

shared_ptr stockFactory(new StockFactory);

 

万事俱备,可以从 this 变 身 shared_ptr 了。

 

shared_ptr StockFactory::get(const string& key)

{

  // change

  // pStock.reset(new Stock(key),

  //               boost::bind(&StockFactory::deleteStock, this, _1));

  // to

  pStock.reset(new Stock(key),

                boost::bind(&StockFactory::deleteStock,

                             shared_from_this(),

                             _1));

  // ...

 

这样一来,boost::function 里保存了一份 shared_ptr,可以保证调用 StockFactory::deleteStock 的时候那个 StockFactory 对象还活着。

 

注意一点,shared_from_this() 不能在构造函数里调用,因为在构造 StockFactory 的时候,它还没有被交给 shared_ptr 接管。

 

最后一个问题,StockFactory 的生命期似乎被意外延长了。


弱回调

 

把 shared_ptr 绑 (bind) 到 boost:function 里,那么回调的时候对象始终存在,是安全的。这同时也延长了对象的生命期,使之不短于 boost:function 对象。

 

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

 

使用这一技术的完整 StockFactory 代码如下:

 

 

  1. class StockFactory : public boost::enable_shared_from_this,  
  2.                        boost::noncopyable  
  3. {  
  4.  public:  
  5.   shared_ptr get(const string& key)  
  6.   {  
  7.     shared_ptr pStock;  
  8.     MutexLock lock(mutex_);  
  9.     weak_ptr& wkStock = stocks_[key];  
  10.     pStock = wkStock.lock();  
  11.     if (!pStock) {  
  12.       pStock.reset(new Stock(key),  
  13.                    boost::bind(&StockFactory::weakDeleteCallback,  
  14.                                boost::weak_ptr(shared_from_this()),  
  15.                                _1));  
  16.       // 上面必须强制 把 shared_from_this() 转型为 weak_ptr,才不会延长生命期  
  17.        wkStock = pStock;  
  18.     }  
  19.     return pStock;  
  20.   }  
  21. private:  
  22.   static void weakDeleteCallback(boost::weak_ptr wkFactory,  
  23.                                  Stock* stock)  
  24.   {  
  25.     shared_ptr factory(wkFactory.lock());  
  26.     if (factory) {  // 如果 factory 还在,那就清理 stocks_  
  27.       factory->removeStock(stock);  
  28.     }  
  29.     delete stock;  // sorry, I lied  
  30.   }  
  31.   void removeStock(Stock* stock)   
  32.   {  
  33.     if (stock) {  
  34.       MutexLock lock(mutex_);  
  35.       stocks_.erase(stock->key());  
  36.     }  
  37.   }  
  38.  private:  
  39.   std::map > stocks_;  
  40.   mutable Mutex mutex_;  
  41. };  

 

两个简单的测试:

 

 

  1. void testLongLifeFactory()  
  2. {  
  3.   shared_ptr factory(new StockFactory);  
  4.   {  
  5.     shared_ptr stock = factory->get("NYSE:IBM");  
  6.     shared_ptr stock2 = factory->get("NYSE:IBM");  
  7.     assert(stock == stock2);  
  8.     // stock destructs here  
  9.   }  
  10.   // factory destructs here  
  11. }  
  12. void testShortLifeFactory()  
  13. {  
  14.   shared_ptr stock;  
  15.   {  
  16.     shared_ptr factory(new StockFactory);  
  17.     stock = factory->get("NYSE:IBM");  
  18.     shared_ptr stock2 = factory->get("NYSE:IBM");  
  19.     assert(stock == stock2);  
  20.     // factory destructs here  
  21.   }  
  22.   // stock destructs here  
  23. }  

 

这下完美了,无论 Stock 和 StockFactory 谁先挂掉都不会影响程序的正确运行。

 

当然,通常 Factory 对象是个 singleton,在程序正常运行期间不会销毁,这里只是为了展示弱回调技术,这个技术在事件通知中非常有用。


11 替代方案?

 

除了使用 shared_ptr/weak_ptr,要想在 C++ 里做到线程安全的对象回调与析构,可能的办法有:

 

1. 用一个全局的 facade 来代理 Foo 类型对象访问,所有的 Foo 对象回调和析构都通过这个 facade 来做,也就是把指针替换为 objId/handle。这样理论上能避免 race condition,但是代价很大。因为 要想把这个 facade 做成线程安全,那么必然要用互斥锁。这样一来,从两个线程访问两个不同的 Foo 对 象也会用到同一个锁,让本来能够并行执行的函数变成了串行执行,没能发挥多核的优势。当然,可以像 Java 的 ConcurrentHashMap 那样用多个 buckets,每个 bucket 分别加锁,以降低 contention

 

2. 自己编写引用计数的 handle。本质上是重新发明轮子,把 shared_ptr 实现一遍。正确实现线程安全的引用计数智能指针不是一件容易的事情,而高效的实现就更加困难。既然shared_ptr 已经提供了完整的解决方案,那么似乎没有理由抗拒它。

 

3. 将来在 C++ 0x 里有 unique_ptr,能避免引用计数的开销,或许能在某些场合替换shared_ptr


其他语言怎么办

 

有垃圾回收就好办。Google 的 Go 语言教程明确指出,没有垃圾回收的并发编程是困难的(Concurrency is hard without garbage collection。但是由于指针算术的存在,在 C/C++里实现全自动垃圾回收更加困难。而那些天生具备垃圾回收的语言在并发编程方面具有明显的优势,Java 是目前支持并发编程最好的主流语言,它的 util.concurrent 库和内存模型是 C++ 0x 效仿的对象。


12 心得与总结

 

学习多线程程序设计远远不是看看教程了解 API 怎 么用那么简单,这最多“主要是为了读懂别人的代码,如果自己要写这类代码,必须专门花时间严肃认真系统地学习,严禁半桶水上阵”(孟岩)。一般的多线程教程上都会提到要让加锁的区域足够小, 这没错,问题是如何找出这样的区域并加锁,本文第 节举的安全读写 shared_ptr 可算是一个例子。

 

据我所知,目前 C++ 没 有好的多线程领域专著,语言有,Java 语言也有。《Java Concurrency in Practice》是我读过的写得最好的书,内容足够新,可读性和可操作性俱佳。C++ 程序员反过来要向 Java 学习,多少有些讽刺。除了编程书,操作系统教材也是必读的,至少要完整地学习一本经典教材的相关章节,可从《操作系统设计与实现》、《现代操作 系统》、《操作系统概念》任选一本,了解各种同步原语、临界区、竞态条件、死锁、典型的 IPC 问 题等等,防止闭门造车。

 

分析可能出现的 race condition 不仅是多线程编程基本功,也是设计分布式系统的基本功,需要反复历练,形成一定的思考范式,并积累一 些经验教训,才能少犯错误。这是一个快速发展的领域,要不断吸收新知识,才不会落伍。单 CPU 时 代的多线程编程经验到了多 CPU 时代不一定有效,因为多 CPU 能做到真正的并发执行,每个 CPU 看 到的事件发生顺序不一定完全相同。正如狭义相对论所说的每个观察者都有自己的时钟,在不违反因果律的情况下,可能发生十分违反直觉的事情。

 

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


不用跨线程的对象,自然不会遇到本文描述的各种险态。如果迫不得已要用,我希望本文能对您有帮助。


总结

 

原始指针暴露给多个线程往往会造成 race condition 或额外的簿记负担。

统一用 shared_ptr/scoped_ptr 来管理对象的生命期,在多线程中尤其重要。

shared_ptr 是值语 意,当心意外延长对象的生命期。例如 boost::bind 和容器。

weak_ptr 是 shared_ptr 的好搭档,可以用作弱回调、对象池等。

认真阅读一遍 boost::shared_ptr 的文档,能学到很多东西。

保持开放心态,密切注意更好的解决办法,比如 unique_ptr。忘掉已被废弃的 auto_ptr

 

shared_ptr 是 tr1 的一部分,即 C++ 标准库的一部分,值得花一点时间去学习掌 握,对编写现代的 C++ 程序有莫大的帮助。我个人的经验是,一周左右就能基本掌握各种用法与常见陷阱,比学 STL 还 快。网络上有一些对 shared_ptr 的批评,那可以算作故意误用的例子,就好比故意访问失效的迭代器来证明 vector 不安全一样。

 

正确使用 shared_ptr,从此告别内存错误。


13 附录:Observer 之谬

 

本文第 节 把 shared_ptr/weak_ptr 应用到 Observer 模式中,部分解决了其线程安全问题。我用 Observer 举例,因为这是一个广为人知的设计模式,但是它有本质的问题。

 

Observer 模式的本质问题在于其面向对象的设计。换句话说,我认为正是面向对象 (OO) 本 身造成了 Observer 的缺点。Observer 是基类,这带来了非常强的耦合,强度仅次于友元。这种耦合不仅限制了成员函数的名字、参数、返回 值,还限制了成员函数所属的类型(必须是 Observer 的派生类)。

 

Observer 是基类,这意味着如果 Foo 想要观察两个类型的事件(比如时钟和温 度),需要使用多继承。这还不是最糟糕的,如果要重复观察同一类型的事件(比如 秒钟一次的心跳和 30 秒 钟一次的自检),就要用到一些伎俩来 work around,因为不能从一个 Base class 继承两次。

 

现在的语言一般可以绕过 Observer 模式的限制,比如 Java 可以用匿名内部类,Java 7 用 ClosureC# 用 delegateC++ 用 boost::function/ boost::bind,我在另外一篇博客《以 boost::function 和 boost:bind 取代虚 函数》里 有更多的讲解。

 

在 C++ 里 为了替换 Observer,可以用 Signal/Slots,我指的不是 QT 那种靠语言扩展的实现,而是完全靠标准库实现 的 thread safe 的、没有 race condition 的、没有 thread contention 的 Signal/Slots,并且不强制要求 shared_ptr 来管理对象,也就是说完全解 决了第 节列出的 Observer 遗留问题。不过这篇文章已经够长了,留作下次吧。有兴趣的同学可以先预习一下《借 shared_ptr 实现线 程安全的 copy-on-write

14 后记

 

C++ 沉思录》/Runminations on C++》中文版的附录是王曦和孟岩对作者夫妇二人的采访,在被问到“请给我们三个你们认为最重要的建议” 时,Koenig 和 Moo 的第一个建议是“避免使用指针”。我 2003 年 读到这段时,理解不深,觉得固然使用指针容易造成内存方面的问题,但是完全不用也是做不到的,毕竟 C++ 的 多态要透过指针或引用来起效。年之后重新拾起来,发现大师的观点何其深刻,不m免掩卷长叹。

 

这本书详细地介绍了 handle/body idiom,这是编写大型 C++ 程序的必备技术,也是实现物理隔离的法宝, 值得细读。

 

 

目前来看,用 shared_ptr 来管理资源在国内 C++ 界似乎并不是一种主流 做法,很多人排斥智能指针(这或许受了 auto_ptr 的垃圾设计的影响)。据我所知,很多 C++ 项目还是手动管理,因此我觉得有必要把我认为好的做法分享出来,让更多的人尝试并采纳。我觉得 shared_ptr 对于编写线程安全的 C++ 程序是至关重要的,不 然就得土法炼钢,自己重新发明轮子。这让我想起了 2001 年前后 STL 刚刚传入国内,大家也是很犹豫,觉得它性能 不高,使用不便,还不如自己造的容器类。近十年过去了,现 在 STL 已经是主流,大家也适应了迭代器、容器、算法、适配器、仿函数这些“新”名词“新”技术,开始在项目 中普遍使用。我希望,几年之后人们回头看这篇文章,觉得“怎么讲的都是常识”,那我这篇文章的目的也就达到了。

阅读(2268) | 评论(0) | 转发(0) |
0

上一篇:C++中线程安全的对象回调 1

下一篇:cstdlib

给主人留下些什么吧!~~