对象析构与线程安全

对象析构与线程安全

本文从C++程序设计的角度分析对象在析构的时候存在的线程安全问题,并以观察者模式的实现为例,提出了不同的解决办法。

线程安全问题的暴露


如果将一个对象的指针注册到另一个对象的成员函数回调中,那么在多线程环境下就存在竞态条件,以观察者模式的实现为例说明,在Observer的基类中,保存了被观察者的指针subject_,用于在本观察者对象析构的时候,从观察列表中取消观察该主题(注释(1)处),用以保证在被观察事件发生的时候通知各个观察者时,保证本观察者存在(注释(2))。

如果Observer对象析构的时候不把本对象指针从观察者列表中移除,那么调用notify的时候,遍历到该对象的指针,由于对象已经析构,指针多指向的对象已经不存在,程序就会出现core dump。

如果在对象析构的时候不移除,而是把列表中对应的指针设置为NULL,仍然不能保证程序按照预期的执行,因为此时可能有新的对象生成,占用了同样的地址空间,那么新对象就会得到通知。

上述要解决的中心问题就是调用notify 的时候,observer对象是否还存在,这里可以使用智能指针来解决,依靠引用计数,可以判断当前对象是否还有引用存在,如果有,就不能析构,从而保证了对象的生存期安全。

另外一个竞态条件是,如果一个线程正要通知的对象,此时正在被另一个线程detach撤销不关注本主题,也即代码中的(2)和(3)发生竞态条件,这个比较容易解决,可以通过加锁的方式实现。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class Observable;

class Observer{
    public:
        ~Observer();
        virtual void update() = 0;
        void observe(Observable* sub);
    private:
        Observable* subject_;
};


class Observable{
    public:
        void attach(Observer* obser){
            observerList_.push_back(obser);
        }

        void detach(Observer* obser){
            vector<Observer*>::iterator it = find(
                    observerList\_.begin(), observerList_.end(), obser);
            if(it != observerList_.end()){
                swap(*it, observerList_.back());//(3)
                observerList_.pop_back();
            }
        }

        void notify(){
            vector<Observer*>::iterator it = observerList_.begin();
            for(; it != observerList_.end(); ++it){
                if(*it)             //(2)
                    (*it)->update();
            }
        }

    private:
        vector<Observer*> observerList_;
};

Observer::~Observer(){
    subject_->detach(this); //(1)
    cout << "Observer destruct" << endl;
}

void Observer::observe(Observable* sub){
    subject_ = sub;
    subject_->attach(this);
}

class Listener : public Observer{
    public:
        virtual void update(){
            cout << "Listener Update" << endl;;
        }
};

使用智能指针和互斥锁解决


上面对问题的描述和解决方法已经描述的比较清楚了,这里通过程序来展示一下修正后的观察者模式。

在观察者数组中使用weak_ptr来代替原生指针,通过引用计数来控制对象的生存期,当引用计数为0时,对象自动销毁,这样就可以保证在调用notify的时候,数组中的观察者对象一定没有被销毁。

这里之所以使用shared_ptr来管理观察者对象,是因为shared_ptr容易造成循环引用,即两个对象互相引用对方,使得最终双方都无法析构。而weak_ptr很好的解决了这个问题,它不增加引用计数,当需要的时候可以上升为shared_ptr,调用成员函数,如果此时对象已析构,则上升失败,智能指针为空。由此可以很方便的判断对象的生存期,并且不需要在析构的时候专门从数组中移除对应的观察者对象指针。

为了保证多线程环境下的观察者列表的线程安全性,增加了互斥锁,在改变该列表状态时都要提前加锁。

class Observable;
class Observer : public enable_shared_from_this<Observer>{
    public:
        ~Observer();
        virtual void update() = 0;
        void observe(Observable* sub);
    private:
        Observable* subject_;
};


class Observable{
    public:
        ~Observable(){
            cout << "Observable destruct" << endl;
        }
        void attach(weak_ptr<Observer> obser){
            MutexLockGuard lock(mutex_); 
            observerList_.push_back(obser);
        }

        void notify(){
            MutexLockGuard lock(mutex_); 
            vector<weak_ptr<Observer> >::iterator it = observerList_.begin();
            while(it != observerList_.end()){
                shared_ptr<Observer> obj(it->lock());
                if(obj){
                    obj->update();
                    ++it;
                }
                else{
                    cout << "observer already destroyed" << endl;
                    it = observerList_.erase(it);
                }
            }
        }

    private:
        mutable MutexLock mutex_;
        vector<weak_ptr<Observer> > observerList_;
};

Observer::~Observer(){
    cout << "Observer destruct" << endl;
}

void Observer::observe(Observable* sub){
    subject_ = sub;
    sub->attach(shared_from_this());
}

class Listener : public Observer{
    public:
        virtual void update(){
            cout << "Listener Update" << endl;;
        }
};


int main(int argc, const char *argv[])
{
    Observable subject;
    {   
        shared_ptr<Listener> listener(new Listener);
        listener->observe(&subject);
        subject.notify();
    }
    subject.notify();

    return 0;
}

安全的运行图

智能指针带来的问题


这里使用了weak_ptr,那么程序就必须使用shared_ptr来管理,但是鉴于引用计数的线程安全性,被观察者类的成员函数就必须加锁,这就造成了锁争用,并且notify的争用时间较长,这对程序的效率就会造成影响。

原文链接 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值