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 class Observer // : boost::noncopyable
- 2 {
- 3 public:
- 4 virtual ~Observer();
- 5 virtual void update() = 0;
- 6 // ...
- 7 };
- 8
- 9 class Observable // : boost::noncopyable
- 10 {
- 11 public:
- 12 void register_(Observer* x);
- 13 void unregister(Observer* x);
- 14
- 15 void notifyObservers() {
- 16 for (Observer* x : observers_) { // 这行是C++(www.cppentry.com)11
- 17 x->update(); // (3)
- 18 }
- 19 }
- 20 private:
- 21 std::vector<Observer*> observers_;
- 22 };
当Observable 通知每一个Observer 时(L17),它从何得知Observer 对象x 还活着?要不试试在Observer 的析构函数里调用unregister() 来解注册?恐难奏效。- 23 class Observer
- 24 {
- 25 // 同前
- 26 void observe(Observable* s) {
- 27 s->register_(this);
- 28 ssubject_ = s;
- 29 }
- 30
- 31 virtual ~Observer() {
- 32 subject_->unregister(this);
- 33 }
- 34
- 35 Observable* subject_;
- 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() 之类的程序函数,告诉我们那个对象还在不在。可惜指针和引用都不是对象,它们是内建类型。