《Linux多线程服务端编程》—线程安全的对象生命期管理

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

线程安全的定义

依据[JCP],一个线程安全的class应当满足以下三个条件:

  1. 多个线程同时访问时,其表现出正确的行为。
  2. 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织。
  3. 调用端代码无须额外的同步或其他协调动作。

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

对象的构造

对象构造要做到线程安全,唯一的要求就是在构造期间不要泄露this指针,即:

  1. 不要在构造函数中注册任何回调。
  2. 不要在构造函数中把this传给跨线程的对象。
  3. 即便在构造函数的最后一行也不行。

因为在构造函数执行期间对象还没有完成初始化,如果this被泄露给了其他对象,那么别的线程就有可能访问到这个半成品对象,从而造成难以预料的后果。

关于“即便是最后一行也不行”,是因为如果该类是一个基类,由于基类先于派生类构造,所以执行完该类的构造函数最后一行代码后,还会继续执行派生类的构造函数。

对象的析构

一个万能的解决方案——引入另外一层间接性,用对象来管理共享资源。

神器shared_ptr/weak_ptr

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容器一样。(后面会再讨论)

应用于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 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 x);
    //void unregister(weak_ptr x); //不需要它

    void notifyObservers();

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

void Observable::notifyObservers();
{
    MutexLockGuard lock(mutex_);
    Iterator it = observers_.begin();
    while (it != observers_.end())
    {
        shared_ptr obj(it->lock()); //尝试提升,线程安全
        if (obj) //提升成功,现在引用计数至少为2(成功说明原来至少为1,提升之后obj为其增加了计数1)
        {
            obj->update(); //没有竞态,因为obj在栈上,对象不可能在本作用域内销毁
            ++it;
        }
        else //提升失败,对象已经销毁
        {
            it = observers_.erase(it);
        }
    }
}

上述用weak_ptr< Observer >代替Observer*部分解决了Observer模式的线程安全问题,但是还有一些疑点:

  1. 侵入性:强制要求Observer 必须以shared_ptr来管理;
  2. 锁争用:Observable的三个成员函数都用互斥器来同步。
  3. 死锁:万一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一样,即:

  1. 一个shared_ptr对象实体可被多个线程同时读取
  2. 两个shared_ptr对象实体可以被两个线程同时写入,“析构”算写操作;
  3. 如果要从多个线程读写同一个shared_ptr对象,那么需要加锁。

注意,以上是shared_ptr对象本身的线程安全级别,不是其管理的对象的线程安全级别。

RAII(资源获取即初始化)

初学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。

多线程编程的建议

用流水线、生产者消费者、任务队列这些有规律的机制,最低限度地共享数据。

建议阅读

《C++沉思录》

孟岩的《function/bind的救赎(上)》
(http://blog.csdn.net/myan/article/details/5928531)

你可能感兴趣的:(C/C++,多线程)