设为首页 加入收藏

TOP

1.4 线程安全的Observer 有多难
2013-10-07 16:00:57 来源: 作者: 【 】 浏览:58
Tags:1.4 线程 安全 Observer 多难

1.4 线程安全的Observer 有多难

一个动态创建的对象是否还活着,光看指针是看不出来的(引用也一样看不出来)。指针就是指向了一块内存,这块内存上的对象如果已经销毁,那么就根本不能访问[CCS,条款](就像free(3) 之后的地址不能访问一样),既然不能访问又如何知道对象的状态呢?换句话说,判断一个指针是不是合法指针没有高效的办法,这是C/C++(www.cppentry.com) 指针问题的根源。(万一原址又创建了一个新的对象呢?再万一这个新的对象的类型异于老的对象呢?)

在面向对象程序设计中,对象的关系主要有三种:composition、aggregation、association。composition(组合/复合)关系在多线程里不会遇到什么麻烦,因为对象x 的生命期由其唯一的拥有者owner 控制,owner 析构的时候会把x 也析构掉。从形式上看,x 是owner 的直接数据成员,或者scoped_ptr 成员,抑或owner 持有的容器的元素。

后两种关系在C++(www.cppentry.com) 里比较难办, 处理不好就会造成内存泄漏或重复释放。

association(关联/联系)是一种很宽泛的关系,它表示一个对象a 用到了另一个对象b,调用了后者的成员函数。从代码形式上看,a 持有b 的指针(或引用),但是b的生命期不由a 单独控制。aggregation(聚合)关系从形式上看与association 相同,除了a 和b 有逻辑上的整体与部分关系。如果b 是动态创建的并在整个程序结束前有可能被释放,那么就会出现§1.1 谈到的竞态条件。

那么似乎一个简单的解决办法是:只创建不销毁。程序使用一个对象池来暂存用过的对象,下次申请新对象时,如果对象池里有存货,就重复利用现有的对象,否则就新建一个。对象用完了,不是直接释放掉,而是放回池子里。这个办法当然有其自身的很多缺点,但至少能避免访问失效对象的情况发生。

这种山寨办法的问题有:

对象池的线程安全,如何安全地、完整地把对象放回池子里,防止出现“部分放回”的竞态?(线程A 认为对象x 已经放回了,线程B 认为对象x 还活着。)

全局共享数据引发的lock contention,这个集中化的对象池会不会把多线程并发的操作串行化?

如果共享对象的类型不止一种,那么是重复实现对象池还是使用类模板?

会不会造成内存泄漏与分片?因为对象池占用的内存只增不减,而且多个对象池不能共享内存(想想为何)。

回到正题上来,如果对象x 注册了任何非静态成员函数回调,那么必然在某处持有了指向x 的指针,这就暴露在了race condition 之下。

一个典型的场景是Observer 模式(代码见recipes/thread/test/Observer.cc)。

  1. 1 class Observer // : boost::noncopyable  
  2. 2 {  
  3. 3 public:  
  4. 4 virtual ~Observer();  
  5. 5 virtual void update() = 0;  
  6. 6 // ...  
  7. 7 };  
  8. 8  
  9. 9 class Observable // : boost::noncopyable  
  10. 10 {  
  11. 11 public:  
  12. 12 void register_(Observer* x);  
  13. 13 void unregister(Observer* x);  
  14. 14  
  15. 15 void notifyObservers() {  
  16. 16 for (Observer* x : observers_) { // 这行是C++(www.cppentry.com)11  
  17. 17 x->update(); // (3)  
  18. 18 }  
  19. 19 }  
  20. 20 private:  
  21. 21 std::vector<Observer*> observers_;  
  22. 22 }; 

当Observable 通知每一个Observer 时(L17),它从何得知Observer 对象x 还活着?要不试试在Observer 的析构函数里调用unregister() 来解注册?恐难奏效。
  1. 23 class Observer  
  2. 24 {  
  3. 25 // 同前  
  4. 26 void observe(Observable* s) {  
  5. 27 s->register_(this);  
  6. 28 ssubject_ = s;  
  7. 29 }  
  8. 30  
  9. 31 virtual ~Observer() {  
  10. 32 subject_->unregister(this);  
  11. 33 }  
  12. 34  
  13. 35 Observable* subject_;  
  14. 36 };  

我们试着让Observer 的析构函数去调用unregister(this),这里有两个raceconditions。其一:L32 如何得知subject_ 还活着?其二:就算subject_ 指向某个永久存在的对象,那么还是险象环生:

1. 线程A 执行到L32 之前,还没有来得及unregister 本对象。

2. 线程B 执行到L17,x 正好指向是L32 正在析构的对象。

这时悲剧又发生了,既然x 所指的Observer 对象正在析构,调用它的任何非静态成员函数都是不安全的,何况是虚函数5。更糟糕的是,Observer 是个基类,执行到L32 时,派生类对象已经析构掉了,这时候整个对象处于将死未死的状态,coredump 恐怕是最幸运的结果。

这些race condition 似乎可以通过加锁来解决,但在哪儿加锁,谁持有这些互斥锁,又似乎不是那么显而易见的。要是有什么活着的对象能帮帮我们就好了,它提供一个isAlive() 之类的程序函数,告诉我们那个对象还在不在。可惜指针和引用都不是对象,它们是内建类型。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇1.3.2 作为数据成员的mutex 不能.. 下一篇1.5 原始指针有何不妥(1)

评论

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

·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)