第一章——读书笔记

线程安全的对象生命期管理

1.1析构函数遇到多线程

当一个对象能被多个线程同时看到时,那么对象的销毁时机就会变得模糊不清,可能出现多种竞态条件。

  • 在即将析构一个对象时,从何而知此可是否有别的线程正在执行该对象的成员函数?
  • 如何保证在执行成员函数期间,对象不会在另一个线程被析构?
  • 在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会碰巧执行到一半?

1.1.1线程安全的定义

一个线程安全的类应该满足以下三个条件

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

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

1.2对象的创建很简单

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

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

之所以这样规定,是因为在构造函数执行其期间对象还没有完成初始化,如果this被泄漏给了其他对象(其自身创建的子对象除外),那么别的线程有可能访问这个半成品对象,这会造成难以预料的后果。

//不要这么做
class Foo : public Observer
{
public:
	Foo(Observer* s)
    {
        s->register_(this);	//错误,非线程安全
    }
    virtual void update();
};

对象构造的正确方法

class Foo : public Observer
{
public:
    Foo();
    virtual void update();
    
    //另外定义一个函数,在构造之后执行回调函数的注册工作
    void observe(Observable* s)
    {
        s->register_(this);
    }
};

Foo* pFoo = new Foo;
Observable* s = getSubject();
pFoo->observe(s);		//二段式构造

二段式构造——即构造函数+initialize()。调用方依靠initialize()的返回值来判断对象是否构造成功,这能简化错误处理。

即使构造函数的最后一行也不要泄漏this,因为基类先于派生类构造,执行完Foo::Foo()的最后一行代码还会继续执行派生类的构造函数,这时most-derived class的对象还处于构造中,仍然不安全。

1.3销毁太难

对象析构,这在单线程里面不构成问题,最多需要注意避免空悬指针和野指针。而在多线程程序中,存在了太多的竞态条件。

成员函数用来保护临界区的互斥器本身必须是有效的,而析构函数破坏了这一假设,它会把mutex成员变量销毁掉。

1.3.1mutex不是办法

mutex只能保证函数一个接一个地执行,考虑下面的代码,它试图用互斥锁来保护析构函数(注意代码中的(1)和(2)两处标记):

Foo::~Foo()
{
	MutexLockGuard lock(mutex_);
    //free internal state 	(1)
}

void Foo::update()
{
	MutexLockGuard lock(mutex_);	//(2)
    //make use of internal state
}

此时,有A,B两个线程都能看到Foo对象x,线程A即将销毁x,而线程B准备调用x->update()

extern Foo* x;	//visible by all threads

//thread A
delete x;
x = NULL;

//thread B
if(x)
{
    x->update();
}

尽管线程A在销毁对象之后把指针置为了NULL,尽管线程B在调用x的成员函数之前检查了指针x的值,但还是无法避免一种race condition

  1. 线程A执行到了析构函数的(1)处,已经持有了互斥锁,即将继续往下执行。
  2. 线程B通过了if(x)检测,阻塞在(2)处。

因为析构函数会把mutex_销毁,那么(2)处有可能永远阻塞下去,有可能进入临界区,然后core dump,或者发生其他更糟糕的情况。

1.3.2作为数据成员的mutex不能保护析构

作为类数据成员的MutexLock只能用于同步本class的其他数据成员的读和写,它不能保护安全地析构。因为MutexLock成员的生命期最多与对象一样长,而析构动作可以说是发生在对象身故之后。另外,对于基类对象,那么调用到基类析构函数的时候,派生类对象的那部分已经析构了,那么基类对象拥有的MutexLock不能保护整个析构过程。再说,析构过程本来也不需要保护,因为只有别的线程都访问不到这个对象时,析构才是安全的。

另外如果要同时读写一个class的两个对象,有潜在的死锁可能。比方说swap()函数。

void swap(Counter& a, Counter& b)
{
    MutexLockGuard aLock(a.mutex_);	//potential dead lock
    MutexLockGuard bLock(b.mutex_);
    int64_t value = a.value_;
    a.value_ = b.value_;
    b.value_ = value;
}

如果线程A执行swap(a, b);而同时线程B执行swap(b,a);就有可能死锁。

operator=()也是类似的道理

Counter& Counter::operator=(const Counter& rhs)
{
    if(this == &rhs)
        return *this;
    MutexLockGuard myLock(mutex_); 	//potential dead lock
    MutexLockGuard itsLock(rhs.mutex_);
    value_ = rhs.value_;		//改成value_ = rhs.value()会死锁
    return *this;
}

1.4线程安全的Observer有多难

一个动态创建的对象是否还活着,光看指针是看不出来的(引用也一样看不出来)。指针就是指向了一块内存,这块内存上的对象如果已经销毁,那么就根本不能访问,既然不能访问又如何知道对象的状态呢?换句话说,判断一个指针是不是合法指针没有高效的办法,这时C/C++指针问题的根源。(万一原地址又创建了一个新的对象呢?再万一这个新的对象的类型异于老的对象呢?)

如果对象x注册了任何非静态成员函数的回调,那么必然在某处持有了指向x的指针,这就暴露在了race condition之下。

一个典型的场景是Observer模式。

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

class Observer
{
public:
    void register_(Observer* x);
    void unregister(Observer* x);
    
    void notifyObservers()
    {
        for(Observer* x : observers_)
            x->update();	//(3)
    }
 
private:
    std::vector<Observer*> observers_;
};

Observable通知每一个Observer时,它并不知道Observer对象x还活着。

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

试着让Observer的析构函数去调用unregister(this),这里有两个race conditions。其一:32行如何得知subject_还活着?其二:就算subject_指向某个永久存在的对象,那么还是会出问题:

  1. 线程A执行到了12行之前,还没有来得及unregister本对象。
  2. 线程B执行到18行,x正好指向是12行正在析构的对象。

这时悲剧又发生了,既然x所指的Observer对象正在析构,调用它的任何非静态成员函数都是不安全的,何况是虚函数。更糟糕的是,Observer是个基类,执行到12行时,派生类对象已经析构掉了,这时候整个对象处于将死未死的状态。

1.6shared_ptr/weak_ptr

shared_ptr是引用计数型智能指针。shared_ptr<T>是一个类模板,它只有一个类型参数,使用起来很方便。当引用计数降为0时,对象(资源)即被销毁。weak_ptr也是一个引用计数类型指针,但是他不增加对象的引用次数,即弱引用。

  • shared_ptr控制对象的生命期。shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,该x对象就不会析构。当指向对象x的最后一个shared_ptr析构或reset()的时候,x保证会被销毁。
  • weak_ptr不控制对象的生命期,但是它知道对象是否还或者。如果还活着,那么可以提升为有效的shared_ptr;如果对象已经死了,提升会失败,返回一个空的shared_ptr
  • shared_ptr、weak_ptr的计数在主流平台是原子操作,没有用锁。

1.8 应用到Observer上

可以通过weak_ptr能探查对象的生死,那么Observer模式的竞态条件就很容易解决,只要让Observable保存weak_ptr<Observer>即可。

class Observable		//not 100% thread safe
{
public:
    void register_(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
            obj->update();
            ++it;
        }
        else
        {
            //对象已经销毁,从容器中拿掉weak_ptr
            it = observers_.erase(it);
        }
    }
};

1.9shared_ptr的线程安全

shared_ptr本身不是100%线程安全的。它的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr有两个数据成员,读写操作不能原子化。根据文档,shared_ptr的线程安全级别和内建类型,标准库容器,std::string一样,即

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

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

要在多个线程中同时访问同一个shared_ptr,正确的做法是用mutex保护:

MutexLock mutex;
shared_ptr<Foo> globalPtr;

//我们的任务是把globalPtr安全地传送给doit()
void doit(const shared_ptr<Foo>& pFoo);

globalPtr能被多个线程看到,那么它的读写需要加锁。

为了拷贝globalPtr,需要在读取它的时候加锁,即:

void read()
{
    shared_ptr<Foo> localPtr;
    {
        MutexLockGuard lock(mutex);
        localPtr = globalPtr;	//read globalPtr
    }
    //use localPtr since here,读写localPtr也无须加锁
    doit(localPtr);
}

写入的时候也要加锁

void write()
{
    shared_ptr<Foo> newPtr(new Foo);		//注意,对象的创建在临界区之外
    {
        MutexLockGuard lock(mutex);
        globalPtr = newPtr;	//write to globalPtr
    }
    doit(newPtr);
}

注意到上面的read()write()在临界区之外都没有再访问globalPtr,而是使用了一个指向同一个Foo对象的栈上shared_ptr local copy。用reference to const作为参数类型,shared_ptr作为函数参数传递时不必复制。另外注意到上面的new Foo是在临界区之外执行的,这种写法通常比在临界区内写globalPtr.reset(new Foo)要好,因为缩短了临界区长度。如果要销毁对象,我们可以在临界区内执行globalPtr.reset(),但是这样会让对象析构发生在临界区内,增加了临界区的长度。一种改进方法是像上面一样定义一个localPtr,用它在临界区内与globalPtr交换,这样能保证把对象的销毁推迟到临界区之外。

1.10shared_ptr技术与陷阱

意外延长对象的生命期shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,该对象就不会析构。而shared_ptr又是允许拷贝构造和赋值的,如果不小心遗留了一个拷贝,那么对象就永远存在了。

另外一个出错的可能是boost::bind,因为boost::bind会把实参拷贝一份,如果参数是个shared_ptr,那么对象的生命期就不会短于boost::function对象。

class Foo
{
  	void doit();  
};

shared_ptr<Foo> pFoo(new Foo);
boost::function<void()> func = boost::bind(&Foo::doit, pFoo);

这里的func对象持有了shared_ptr<Foo>的一份拷贝。

函数参数:因为要修改引用计数(而且拷贝的时候通常要加锁),shared_ptr的拷贝开销比拷贝原始指针要高,但是需要拷贝的时候并不多。多数情况下可以通过const reference方式传递。

析构动作在创建时被捕获:这意味着

  • 虚析构不再是必需的。
  • shared_ptr<void>可以持有任何对象,而且能安全地释放。
  • shared_ptr可以安全地跨越模块边界,比如从DLL里返回,而不会造成从模块A分配的内存在模块B里被释放这种错误。
  • 析构动作可以定制。

析构所在的线程:对象的析构是同步的,当最后一个指向xshared_ptr离开其作用域的时候,x会同时在同一个线程析构。这个线程不一定是对象诞生的线程。如果对象的析构比较耗时,那么可能会拖慢关键线程的速度。

1.11对象池

假设有Stock类,代表一只股票的价格。每一只股票有一个唯一的字符串标识。

可以设计一个对象池StockFactory,根据key返回Stock对象。在多线程程序中,既然对象可能被销毁,那么返回shared_ptr是合理的。

自然的,我们写出如下代码(可惜是错误的)

//version 1:questionable code
class StockFactory : boost::noncopyable
{
public:
    shared_ptr<Stock> get(const string& key);

private:
	mutable MutexLock mutex_;
    std::map<String, shared_ptr<Stock>> stocks_;
};

get()的逻辑很简单,如果在stocks_里面找到了key,就返回stocks_[key];否则新建一个Stock,并存入stocks_[key]

这里有一个问题,Stock对象永远不会销毁,因为map里存的是shared_ptr。或许应该仿照前面的Observable那样存一个weak_ptr?比如

//version 2 : 数据成员修改为std::map<string,weak_ptr<Stock>> stocks_;
shared_ptr<Stock> StockFactory::get(const string& key)
{
    shared_ptr<Stock> pStock;
    MutexLockGuard lock(mutex_);
    weak_ptr<Stock>& wkStock = stocks_[key];		//如果key不存在,会默认构造一个
    pStock = wkStock.lock();		//尝试提升weak_ptr
    if(!pStock)
    {
   		pStock.reset(new Stock(key));
        wkStock = pStock;			// 这里更新了stocks_[key],注意wkStock
    }
    return pStock;
}

这么做固然Stock对象是销毁了,但是程序却出现了轻微的内存泄漏。因为stocks_的大小只增不减。

解决的办法是,利用shared_ptr的定制析构功能。shared_ptr的构造函数可以有一个额外的模板类型参数,传入一个函数指针或仿函数d,在析构对象时执行d(ptr),其中ptrshared_ptr保存的对象指针。

template<class Y, class D> shared_ptr::shared_ptr(Y* p, D d);
template<class Y, class D> void shared_ptr::reset(Y* p, D d);

//version 3
class StockFactory : boost noncopyable
{
    //在get()中,将pStock.reset(new Stock(key))改为
    //pStock.reset(new Stock(key), 
    //			   boost::bind(StockFactory::deleteStock, this, _1));
private:
    void deleteStock(Stock* stock)
    {
        if(stock)
        {
            MutexLockGuard lock(mutex_);
            stocks_.erase(stock->key());
        }
        delete stock;
    }
}

这里向pStock.reset()传递了第二个参数,一个boost::function,让它在析构Stock* p时调用本StockFactory对象deleteStock成员函数。

这里有一个问题,那就是我们把一个原始的StockFactory this指针保存在了boost::function里,这会有线程安全问题。如果这个StockFactory先于Stock析构,那么会有core dump

1.11.1 enable_shared_from_this

StockFactory::get()把原始指针this保存到了boost::function中,如果StockFactory的生命期比Stock短,那么Stock析构时去回调StockFactory::deleteStock就会core dump

应该使用shared_ptr来解决对象生命期问题,但是StockFactor::get()本身是个成员函数,如何获得一个指向当前对象的shared_ptr<StockFactory>对象呢?

使用enable_shared_from_this。这是一个以其派生类为模板类型实参的基类模板,继承它,this指针就能变身为shared_ptr

class StockFactory : public boost::enable_shared_from_this<StockFactory>, 
						boost::noncopyable
                        {}

为了使用shared_from_this()StockFactory不能是stack object,必须是heap object且由shared_ptr管理其生命期

shared_ptr<StockFactory> stockFactory(new StockFactory);

可以使this变成shared_ptr<StockFactory>

//version 4
shared_ptr<Stock> StockFactory::get(const string& key)
{
    //change
    //pStock.reset(new Stock(key),
    //				boost::bind(&StockFactory::deleteStock, this, _1));
    //to
    pStock.reset(new Stock(key),
                boost::bind(&StockFactoyr::deleteStock,
                           shared_from_this(),
                           _1));
    //....
}

这样一来,boost::function保存了一份shared_ptr<StockFactory>,可以保证调用StockFactory::deleteStock的时候StockFactory对象还活着。

1.11.2 弱回调

shared_ptr绑(boost::bind)到boost::function里面,那么回调的时候StockFactory对象始终存在,是安全的。这同时也延迟了对象的生命期,使之不短于绑定的boost::function对象。

有时候我们需要“如果对象还活着,就调用它的成员函数,否则忽略之”的语义,就像Observeable::notifyObservers()那样,称之为==“弱回调”==。利用weak_ptr,可以把weak_ptr绑定到boost::function里面,这样生命期就不会被延长。然后在回调的时候先尝试提升为shared_ptr,如果提升成功,说明接收回调的对象还健在,那么就执行回调;

class StockFactory:public boost::enable_shared_from_this<StockFactory>, boost::noncopyable
{
public:
    shared_ptr<Stock> get(const string& key)
    {
        shared_ptr<Stock> pStock;
        MutexLockGuard lock(mutex_);
        weak_ptr<Stock>& wkStock = stocks_[key];
        pStock = wkStock.lock();
        if(!pStock)
        {
            pStock.reset(new Stock(key),
                        boost::bind(&StockFactory::weakDeleteCallback,
                                   boost::weak_ptr<StockFactory>(shared_from_this()),_1));
            wkStock = pStock;
        }
        return pStock;
    }

private:
    static void weakDeleteCallback(const boost::weak_ptr<StockFactory>& wkFacory, Stock* stock)
    {
        shared_ptr<StockFactory> factory(wkFactory.lock());
        if(factory)
        {
            factory->removeStock(stock);
        }
        delete stock;
    }
    
    void removeStock(Stock* stock)
    {
        if(stock)
        {
            MutexLockGuard lock(mutex_);
            stocks_.erase(stock->key());
        }
    }

private:
	mutable MutexLock mutex_;
    std::map<string, weak_ptr<Stock>> stocks_;
}         

两个简单的测试

void testLongLifeFactory()
{
    shared_ptr<StockFactory> factory(new StockFactory);
    {
        shared_ptr<Stock> stock = factory->get("NYSE:IBM");
        shared_ptr<Stock> stock2 = factory->get("NYSE:IBM");
        assert(stock == stock2);
        //stock destructs here
    }
    //factory destructs here
}

void testShortLifeFactory()
{
    shared_ptr<Stock> stock;
    {
        shared_ptr<StockFactory> factory(new StockFactory);
        stock = factory->get("NYSE:IBM");
        shared_ptr<Stock> stock2 = factory->get("NYSE:IBM");
        assert(stock == stock2);
        //factory destructs here
    }
    //stock destructs here
}

这样,无论StockStockFactory谁先挂掉都不会影响程序的正确运行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值