前言
在c++多线程编程中,线程间可能会使用跨线程对象,那么对象在什么时候销毁便成了一个让人头疼的问题,因为一个线程在使用这个对象时,另外一个线程可能正在析构这个对象。该章内容就是为我们解读,如何正确的处理跨线程对象的生命周期,以避免内存泄露、空悬指针或者使得程序处于不确定状态等问题。
线程安全
什么是线程安全?这里给出书中的定义:
一个线程安全的class应当满足以下三个条件:
- 多个线程同时访问时,能够表现出正确的行为
- 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织
- 调用段代码无须额外的同步或者其他协调动作
依据这个定义,c++标准库中大多数class都不是线程安全的,包括std::string、std::vector、std::map等,因为这些class通常需要在外部加锁才能供多个线程同时访问
个人的理解就是,一个线程安全的类,即使不用在外部加锁,也可以在多个线程中同时访问,想想,实现这样的类,就很难,这也是为什么多线程编程是我一直想去积累一些东西的原因,这是座山,而我必须得翻过。
多线程中的析构与智能指针
由于在多线程环境中,一个对象可以同时被多个线程访问,这导致析构该对象的时机变得模糊不清。很有可能该对象在一个线程中被访问,而同时在另外一个线程中被析构,这些都会造成程序处于未知的状态。那么我们是否可以加锁来保证析构时,其他线程不会访问呢?遗憾的是,析构时锁作为对象的一部分也会被析构,这样就无法对临界区进行保护。
实际上,我们在多线程编程使用原始指针时,不得不思考一个问题,那就是什么时候对象的析构才是安全的?只有其他线程都访问不到该对象的时候才是安全的。自然而然,我们可以想到使用智能指针来引入一层间接性的引用计数。对于跨线程对象而言,最好使用标准库中的智能指针进行处理,智能指针的核心是引用计数。智能指针的思想符合用对象管理资源的思路,实际上,现代c++程序一般不会出现内存泄露的情况,除非特别情况,一般都会使用容器或者智能指针来管理资源。
用智能指针代替原始指针的一大好处是原始指针无法判断所指内存上的对象是否销毁,而只要有智能指针引用计数不为0,所指涉内存上的对象便不会被销毁。
观察者模式与智能指针
#include <algorithm> #include <vector> #include <stdio.h> class Observable; class Observer { public: virtual ~Observer(); virtual void update() = 0; void observe(Observable* s); protected: Observable* subject_; }; class Observable { public: void register_(Observer* x); void unregister(Observer* x); void notifyObservers() { for (size_t i = 0; i < observers_.size(); ++i) { Observer* x = observers_[i]; if (x) { x->update(); // (3) } } } private: std::vector<Observer*> observers_; }; Observer::~Observer() { subject_->unregister(this); } void Observer::observe(Observable* s) { s->register_(this); subject_ = s; } void Observable::register_(Observer* x) { observers_.push_back(x); } void Observable::unregister(Observer* x) { std::vector<Observer*>::iterator it = std::find(observers_.begin(), observers_.end(), x); if (it != observers_.end()) { std::swap(*it, observers_.back()); observers_.pop_back(); } } // --------------------- class Foo : public Observer { virtual void update() { printf("Foo::update() %p\n", this); } }; int main() { Foo* p = new Foo; Observable subject; p->observe(&subject); subject.notifyObservers(); delete p; subject.notifyObservers(); }
如果对象x注册了任何非静态成员函数回调,那么必然在某处持有指向x的指针,那么就有可能出现数据竞险的可能。
比如陈硕在书中提到的上述代码。有一个观察者基类Observer和派生类Foo,另外有一个可观察对象Observable。Foo调用observer方法观察subject对象,同时会在subject对象的_observers成员变量中加入Foo对象的引用。于是就形成了p和suject互相持有对方的引用。并且上述程序存在很大的问题:
- Observer的析构函数是让所持有的subject去注销自己本身,那么如何能得知其subject是否已经被销毁
- 如果线程A中delete p,析构时尚未对持有的subject来unregister其自身,而线程B中的subject正好在通知其对应的所有观察者呢,这就很不安全
于是,我们可以用智能指针来改进这些东西,让观察者与被观察者互相持有智能指针。但是互相持有智能指针会造成循环引用导致内存无法释放,这就需要使用弱引用 weak_ptr技术来解决循环引用的问题。修改之后的代码如下:
#include <algorithm> #include <vector> #include <stdio.h> #include "../Mutex.h" #include <boost/enable_shared_from_this.hpp> #include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> class Observable; class Observer : public boost::enable_shared_from_this<Observer> { public: virtual ~Observer(); virtual void update() = 0; void observe(Observable* s); protected: Observable* subject_; }; class Observable { public: void register_(boost::weak_ptr<Observer> x); // void unregister(boost::weak_ptr<Observer> x); void notifyObservers() { muduo::MutexLockGuard lock(mutex_); Iterator it = observers_.begin(); while (it != observers_.end()) { boost::shared_ptr<Observer> obj(it->lock()); if (obj) { obj->update(); ++it; } else { printf("notifyObservers() erase\n"); it = observers_.erase(it); } } } private: mutable muduo::MutexLock mutex_; std::vector<boost::weak_ptr<Observer> > observers_; typedef std::vector<boost::weak_ptr<Observer> >::iterator Iterator; }; Observer::~Observer() { // subject_->unregister(this); } void Observer::observe(Observable* s) { s->register_(shared_from_this()); subject_ = s; } void Observable::register_(boost::weak_ptr<Observer> x) { observers_.push_back(x); } //void Observable::unregister(boost::weak_ptr<Observer> x) //{ // Iterator it = std::find(observers_.begin(), observers_.end(), x); // observers_.erase(it); //} // --------------------- class Foo : public Observer { virtual void update() { printf("Foo::update() %p\n", this); } }; int main() { Observable subject; { boost::shared_ptr<Foo> p(new Foo); p->observe(&subject); subject.notifyObservers(); } subject.notifyObservers(); }
这样,unregister函数就可以不用了,因为可以通过weak是否可以升级到shared_ptr来判断对象是否存在,并且也解决了之前所述的竞险问题
后续
本章仍然需要深入的问题有:
p17: 为什么“要求mutex可重入”往往意味着设计上出了问题
智能指针带来的意外延长对象生命周期的问题
弱回调技术
弱回调技术常用封装
handy/body惯用技法
观察者模式
java、c++与python并发模型对比
没有帐号?立即注册