Observer2.0-鸡肋的改进的Observer模式
加上Mutex和MutexGuard
我们需要保护Observerable
中的Observers_
,因为可能多个线程中的Observer
可能会同时注册,或是Observer
销毁后它将从Observers_
当中被删除,那么这些同时操作vector<Observer*> observers_
的操作是线程不安全的,我们需要保证在一个时刻只能有一个线程能操作observers_
。
使用Mutex
作为Observerable
成员变量
因此我们可以想到使用互斥锁来保护Observers_
,当Observerable
中的方法将操作Observers_
时,我们先加锁。显而易见,此互斥锁的生命周期和Observerable
的生命周期相同,因此我们将此互斥锁作为Observerable
的成员变量。同时我们使用**RAII(资源获取即初始化)**来对互斥锁进行管理,我们创建一个Mutex
类,里面有一个互斥锁变量,当Mutex
类在构造时,创建互斥锁资源,当Mutex
类析构时,destroy互斥锁资源。因为c++提供的操作互斥锁的函数是面向过程的,因此我们将加锁和解锁操作封装在Mutex
类中方便调用。
使用MutexGuard
来控制加锁解锁
为了保证在加锁后使用者不会忘记解锁,我们创建一个MutexGuard
类,来控制Mutex
类的加锁和解锁。详细做法如下:一个MutexGuard
类中保存着一个Mutex
类的引用,在MutexGuard
构造函数中,首先使用初始化列表初始化这个引用,然后在构造函数结构体中执行Mutex
的加锁操作。在MutexGuard
的析构函数中执行Mutex
的解锁操作,这样就保证了加锁过后一定不会忘记解锁(记着MutexGuard
一定要是一个栈上对象,离开作用域后会被自动析构)。我们将MutexGuard
设置成Mutex
的友元,Mutex
不能直接被使用。
Mutex
类实现
class MutexGuard;
class Mutex{
public:
/*在构造函数中初始化互斥锁*/
Mutex():mutex_(PTHREAD_MUTEX_INITIALIZER){}
/*在析构函数中销毁互斥锁*/
~Mutex(){
pthread_mutex_destroy(&mutex_);
}
private:
void lock();
void unlock();
friend class MutexGuard;
pthread_mutex_t mutex_;
};
void Mutex::lock(){
pthread_mutex_lock(&mutex_);
}
void Mutex::unlock(){
pthread_mutex_unlock(&mutex_);
}
MutexGuard
类实现
class MutexGuard{
public:
explicit MutexGuard(Mutex& mutex):mutex_(mutex){
mutex_.lock();
}
~MutexGuard(){
mutex_.unlock();
}
private:
Mutex& mutex_;
};
此时的Observer
是线程安全的吗?
- Observerable指针可能会比Observer的指针先失效,这样的话Observer在析构时调用Observerable的detach操作是非法的。
- Observer是一个基类,update函数是在它的派生类中实现的,析构的顺序是先析构子类,再析构父类,如果在析构一个Observer的派生类时,在正好执行到析构父类的时候,cpu被另一个操作Observerable的线程抢占并执行notify函数,由于这时候Observer基类的析构函数还未调用,没有从Observerable中删除自己,但它的派生类已经析构掉了,因此这时,notify函数通过Observer指针来调用update函数(是派生类的方法)就是错误的。
~Observer(){
observerable_->detach_(this);
}
- 广泛加锁,效率低下(加锁的地方太多了,例如我们在notify函数中加锁,当更新Observer的时候,可能这个时间会很长,而其他线程一直在等待)。
- 如果update函数调用了register_函数,将导致程序崩溃。(一个得到互斥锁的线程再次请求加锁)
其实这个解决方案是很鸡肋的,1.0版本的Observer最主要的问题时,我们无法判断一个裸指针指向的对象是否是存活的。我们在3.0版本再给出一个解决方案。