C++ 中线程安全的对象生命期管理概要一

在这里,对《Linux多线程服务端编程》的第一章:线程安全的对象生命期管理的概要整理,这章的具体内容见陈硕的博客:http://blog.csdn.net/Solstice/article/details/5238671。


如何避免对象析构时可能存在的竞态条件(race condition)是C++多线程编程面临的基本问题,可以借助shared_ptr和weak_ptr智能指针完美解决。

当一个对象能被多个线程同时看到时,对象的销毁时机变得模糊不清,可能出现多种竞态条件(race condition): 
    1. 在即将析构一个对象时,从何而知此刻是否有别的线程正在执行该对象的成员函数? 
    2. 如何保证在执行成员函数期间,对象不会在另一个线程被析构? 
    3. 在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会碰巧执行到一半?

解决这些race condition是C++多线程编程面临的基本问题。


线程安全的定义
    依据[JCP],一个线程安全的class应当满足以下三个条件:
         1.多个线程同时访问时,其表现出正确的行为。
         2.无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织。
         3.调用端代码无须额外的同步或其他协调动作。

    依据这个定义,C++标准库里大多数class不是线程安全的,包括std::string、std::vector、std::map等,这些class通常需要在外部加锁才能供多个线程同时访问。


对象的构造

    1.对象构造要做到线程安全,唯一的要求就是在构造期间不要泄露this指针,即:
        *不要在构造函数中注册任何回调;
        *不要在构造函数中把this指针传给跨线程的对象;
    原因:因为在构造函数执行期间对象还没完成初始化,如果this被泄漏给了其他对象,那么别的线程有可能访问这个半成品,结果是无法预测的。关于“即便是最后一行也不行”,是因为如果该类是一个基类,由于基类先于派生类构造,所以执行完该类的构造函数最后一行代码后,还会继续执行派生类的构造函数。

    2:二段式构造:
        即:构造函数 + initialize();
        在initialize()中注册回调函数,把this指针传给其他对象,等等。


对象的析构

    *在多线程中,对象的析构存在太多的竞态条件;

    *成员函数用来保护临界区的互斥器本身必须是有效的,而析构函数恰好会把mutex成员变量销毁掉。

    *作为数据成员的mutex不能保护对象的析构,mutex只能用于同步本class的其它数据成员的读和写。

    一个函数如果要锁住相同类型的多个对象,为了保证始终按照相同的顺序加锁,我们可以比较mutex对象的地址,始终先加锁地址较小的mutex.



判断一个指针是不是合法指针没有高效的办法,是C/C++指针问题的根源。

在面向对象程序设计中,对象的关系有三种:composition(组合)、aggregation(聚合)、association(关联)。composition(组合)关系在多线程里不会遇到什么麻烦。后两种关系在C++里比较难办,处理不好就会造成内存泄漏或重复释放。

一个简单的方法,解决内存泄漏:只创建不销毁。程序使用一个对象池来暂存用过的对象,下次申请对象的时候,如果对象池中有,就重复利用,否则就新建一个。对象使用完,不是直接释放掉,而是放回池中。
    缺点:
        *对象池的安全,如何防止局部的竞态条件;
        *这个全局的对象池会不会把多线程串行化;


有了智能指针,虚析构不再是必须的了;

*shared_ptr是引用计数型智能指针,引用计数降为0时,对象(资源)即被销毁。它是强引用,控制对象的生命期,只要有一个指向x对象的shared_ptr存在,该x对象就不会析构。当指向对象x的最后一个shared_ptr析构或者reset()的时候,x保证会被销毁。

*weak_ptr也是一个引用计数型智能指针,但是它不增加或减少对象的引用次数,即弱引用,它不控制对象的生命期,但是可以知道对象是否还活着。(举个例子,假设有5个shared_ptr引用了对象A,则A的引用计数是5,这个时候如果有一个weak_ptr也引用了A,那么A的引用计数还是5,也就是说,weak_ptr并不增加引用计数,但是由于它指向了A,所以可以知道A的引用计数从而判断它是否还活着。)它没有重载*和->但可以使用lock获得一个可用的shared_ptr对象——对象如果还活着,那么它可以提升(promote)为有效的shared_ptr,如果对象已经死了,提升会失败,返回一个空的shared_ptr。“提升/lock()”行为是线程安全的。
*shared_ptr/weak_ptr的“计数”在主流平台上是原子操作,没有用锁,性能不俗,它们的线程安全级别与std::string和STL容器一样。

*scoped_ptr/shared_ptr/weak_ptr都是值语意

*如果这几种智能指针是对象x的数据成员,而它的模版参数T是个incomplete类型,那么x的析构函数不能是默认的或内联的,必须在.cpp文件里边显式定义,否则会有编译错或运行错(原因见$10.3.2)


应用于Observer模式

Observer模式定义对象间的一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
在Observer 设计模式中,有两种角色:Observer是观察者角色,Observable是被观察目标(subject)角色。
先看看以下的Observer模式:

class Observer // : boost::noncopyable
{
public:
    virtual ~Observer();
    virtual void update() = 0;
    //...
};

class Observable // : boost::noncopyable
{
public:
    void register_(Observer* x);
    void unregister(Observer* x);

    void notifyObservers()
    {
        for (Observer* x : observers_)
        {
            x->update();
        }
    }
private:
    std::vector<Observer*> observers_;
};
当Observable通知每一个Observer时(x->update()),它从何得知Obverser对象x还活着?要不试试在Obverser的析构函数里调用unregister()来解注册?

class Observer // : boost::noncopyable
{
    //同前
    void observe(Observable* s)
    {
        s->register(this);
        subject_ = s;
    }
    virtual ~Observer()
    {
        subject_->unregister(this);
    }

    Observable* subject_;
};
然而这里有两个竞态: 
1. 如何得知subject_还活着? 
2. 就是subject_指向某个永久存在的对象,有可能:线程A执行到Observer的析构函数,但是还没来得及调用unregister,线程B执行到x->update(),x正好指向的是A正在析构的对象。
使用shared_ptr/weak_ptr之后:

class Observable
{
public:
    void register_(weak_ptr<Observer> x);
    //void unregister(weak_ptr<Observer> x); //不需要它

    void notifyObservers();

private:
    mutable MutexLock mutex_;
    std::vector< weak_ptr<Observer> > observers_;
    typedef std::vector< weak_ptr<Observer> >::iterator Iterator;
};

void Observable::notifyObservers();
{
    MutexLockGuard lock(mutex_);
    Iterator it = observers_.begin();
    while (it != observers_.end())
    {
        shared_ptr<Observer> obj(it->lock()); //尝试提升,线程安全
        if (obj) //提升成功,现在引用计数至少为2(成功说明原来至少为1,提升之后obj为其增加了计数1)
        {
            obj->update(); //没有竞态,因为obj在栈上,对象不可能在本作用域内销毁
            ++it;
        }
        else //提升失败,对象已经销毁
        {
            it = observers_.erase(it);
        }
    }
}
上述用weak_ptr< Observer >代替Observer*部分解决了Observer模式的线程安全问题,但是还有一些疑点:
侵入性:强制要求Observer 必须以shared_ptr来管理;
锁争用:Observable的三个成员函数都用互斥器来同步。
死锁:万一update()虚函数中调用了register,如果mutex_是不可重入的,那么会造成死锁,如果是可重入的,程序会面临迭代器失效,因为vector observers_在遍历期间被意外修改了。(一种解决方法是,用可重入的mutex_,把容器换成std::list,并把++it往前挪一行)
(书中提到,为替换Observer,可以用Signal/Slots。)


再论shared_ptr的线程安全
我们使用shared_ptr来实现线程安全的对象释放,但是shared_ptr本身并不是100%线程安全的,其引用计数本身是安全且无锁的,但是对象的读写则不是,因为shared_ptr有两个数据成员(一个是引用计数器,一个是指向对象的指针),读写操作不能原子化。
shared_ptr的线程安全级别跟內建类型、标准库容器、std::string一样,即:
一个shared_ptr对象实体可被多个线程同时读取;
两个shared_ptr对象实体可以被两个线程同时写入,“析构”算写操作;
如果要从多个线程读写同一个shared_ptr对象,那么需要加锁。为什么多线程读写 shared_ptr 要加锁?见:http://blog.csdn.net/solstice/article/details/8547547
注意,以上是shared_ptr对象本身的线程安全级别,不是其管理的对象的线程安全级别。


shared_ptr技术与陷阱
*智能指针的缺点:对象的生命周期被延长了;
*shared_ptr的拷贝开销比原始指针要高,但是需要拷贝的事后并不多,因此多数情况下它可以以const reference方式传递
*析构动作在创建时被捕获,“析构行为”可以是函数指针、仿函数、或者其他什么东西
*析构所在的线程,不一定是对象诞生的线程。如果析构耗时,可能会拖慢关键线程的速度,可以用一个单独的线程专门做析构,通过一个BlockingQueue<shared_ptr(void)>把对象的析构都转移到那个专用线程,从而解放关键线程。
*现成的RAII handle(资源获取即初始化)。
初学C++的教条是“new和delete要配对,new了之后要记得delete”;如果使用RAII(《Effective C++》条款13),要改成“每一个明确的资源配置动作(例如new)都应该在单一语句(《Effective C++》条款17)中执行,并在该语句中立刻将配置获得的资源交给handle对象(如shared_ptr),程序中一般不出现delete”。
*shared_ptr是管理共享资源的利器,需要注意避免循环引用,通常的做法是,owner持有指向child的shared_ptr,child持有指向owner的weak_ptr。




shared_ptr技术与陷阱
*智能指针的缺点:对象的生命周期被延长了;
*shared_ptr的拷贝开销比原始指针要高,但是需要拷贝的事后并不多,因此多数情况下它可以以const reference方式传递
*析构动作在创建时被捕获,“析构行为”可以是函数指针、仿函数、或者其他什么东西
*析构所在的线程,不一定是对象诞生的线程。如果析构耗时,可能会拖慢关键线程的速度,可以用一个单独的线程专门做析构,通过一个BlockingQueue<shared_ptr(void)>把对象的析构都转移到那个专用线程,从而解放关键线程。
*现成的RAII handle(资源获取即初始化)。
初学C++的教条是“new和delete要配对,new了之后要记得delete”;如果使用RAII(《Effective C++》条款13),要改成“每一个明确的资源配置动作(例如new)都应该在单一语句(《Effective C++》条款17)中执行,并在该语句中立刻将配置获得的资源交给handle对象(如shared_ptr),程序中一般不出现delete”。
*shared_ptr是管理共享资源的利器,需要注意避免循环引用,通常的做法是,owner持有指向child的shared_ptr,child持有指向owner的weak_ptr。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X-Programer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值