muduo学习笔记:chapter1 线程安全的对象生命周期管理


1 MutexLock与MutexLockGuard

MutexLock:封装临界区,这是一个简单的资源类,用RAII手法封装互斥器的创建与销毁

MutexLockGuard:封装临界区的进入和退出,即加锁和解锁。MutexLockGuard一般是个栈上对象,它的作用域刚好等于临界区域

这两个类都不允许拷贝构造与赋值

2 空悬指针

在这里插入图片描述
解决方法一:引入一层间接性

在这里插入图片描述
更好的办法:引入引用计数

在这里插入图片描述

3 智能指针

3.1 shared_ptr

当引用计数降为0时,对象(资源)即被销毁

3.2 weak_ptr

不增加对象的引用次数,弱引用

3.3 关键点

在这里插入图片描述

4 C++可能出现的内存问题

4.1 类型

  1. 缓冲区溢出(buffer overrun)
  2. 空悬指针/野指针
  3. 重复释放(double delete)
  4. 内存泄漏(memory leak)
  5. 不配对的new[]/delete
  6. 内存碎片(memory fragmentation)

4.2 解决思路

  1. 缓冲区溢出:用std:vector<char>/std:string或自己编写Buffer class来管理缓冲区,自动记住用缓冲区的长度,并通过成员函数而不是裸指针来修改缓冲区。
  2. 空悬指针/野指针:用shared_ptr/weak_ptr。
  3. 重复释放:用scoped_ptr,只在对象析构的时候释放一次。
  4. 内存泄漏:用scoped_ptr,对象析构的时候自动释放内存。
  5. 不配对的new[]/delete:把new[]统统替换为std::vector/scoped_array
  6. 内存碎片

5 智能指针的应用

应用前可能出现的race condition:

class Observer {
public:
	virtual ~Observer();
    virtual void update() = 0;
    //...
};
class Observable {
public:
    void register_(Observer* x);
    void unregister(Observer* x);
    
    void notifyObservers() {
        for(Observer* x : observers_) {
            x->update(); //#2
        }
    }
private:
    std::vector<Observer*> observers_;
};
class Observer {
	//同前
    void observer(Observerable* s) {
        s->register_(this);
        subject_ = s;
    }
    virtual ~Observer() {
        subject_->unregister(this);	//#1
    }
    
    Observable* subject_;
};

让Observer的析构函数调用unregister(this)存在两个race condition:

  1. #1如何subject_还活着

  2. 即使subject_指向永远存在的对象:

    • 线程A执行到#1之前,还没有unregister本对象
    • 线程B执行到#2,x正好指向#1正在析构的对象

应用后:

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_;	//#1 
    typedef std::vector<wek_ptr<Observer>>::iterator Iterator;
};

void Observable::notifyObservers() {
    MutexLockGuard lock(mutex_);
    Iterator it = observers_.begin();
    while(it != obervers_.end()) {
        share_ptr<Observer> obj(it->lock());
        if(obj) {
            //提升成功,引用计数至少为2
            obj->update(); //没有竞态条件,因为obj在栈上,对象不可能在本作用域内销毁
            ++it;
        }
        else {
            //对象已经销毁,从容器中拿掉weak_ptr;
            it = observers_.erase(it);
        }
    }
}

6 shared_ptr的线程安全

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

  • 一个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
    //读写localPtr无需加锁
    doit(localPtr);
}
//写入的时候也要加锁
void write() {
    shared_ptr<Foo> newPtr(new Foo);	//对象的创建在临界区外,缩短了临界区长度
    MutexLockGuard lock(mutex);
    globalPtr = newPtr;	//write to globalPtr;
    //读写newPtr无需加锁
    doit(newPtr);
}

7 shared_ptr技术与陷阱

7.1 意外延长对象的生命期

  1. shared_ptr是强引用的,只要有一个指向x对象的shared_ptr存在,该对象就不会析构。而shared_ptr又是允许拷贝构造和赋值的(否则引用计数就无意义了),如果不小心遗留了一个拷贝,那么对象就永世长存了。

  2. 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); //long life foo

7.2 函数参数

因为需要修改引用计数,所以shared_ptr的拷贝开销要比拷贝原始指针高,但是需要拷贝的情况不多,多数情况下它可以以const reference方式传递

void save(const shared_ptr<Foo>& pFoo); //pass by const reference
void validateAccount(const Foo& foo);

bool validate(const shared_ptr<Foo>& pFoo) {
    validateAccount(*pFoo);
    //...
}

void onMessage(const string& msg) {
    shared_ptr<Foo>(new Foo(msg));	//一个线程只要在最外层持有一个实体,安全不成问题
    if(validate(pFoo)) {	//没有拷贝pFoo
        save(pFoo);			//没有拷贝pFoo
    }
}

遵照上述规则,基本上不会遇到反复拷贝shared_ptr导致的性能问题。另外由于pFoo是栈上对象,不可能被别的线程看到,那么读取始终是线程安全的。

7.3 析构动作在创建时被捕获

7.4 析构所在的线程

对象的析构是同步的,当最后一个指向x的shared_ptr离开其作用域时,x会同时在同一个线程析构。这个线程不一定是对象诞生的线程。这个特性是把双刃剑:如果对象的析构比较耗时,那么可能会拖慢关键线程的速度(如果最后一个shared_ptr引发的析构发生在关键线程);同时,我们可以用一个单独的线程来专门做析构,通过一个BlockingQueue<shared_ptr<void>>把对象的析构都转移到那个专用线程,从而解放关键线程。

7.5 现成的RAII机制

Resource Acquisition Is Initialization:“资源获得时机便是初始化时机”

每一个明确的资源配置动作(例如new)都应该在单一语句中执行,并在该语句中立刻将配置获得的资源交给handle对象(如shared_ptr),程序中一般不会出现delete

shared_ptr是管理共享资源的利器,需要注意避免循环引用,通常的做法是owner持有指向child的shared_ptr,child持有指向owner的weak_ptr。

8 对象池

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.1 enabled_shared_from_this

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

解决方法:用enable_shared_from_this:一个以其派生类为模板类型实参的基类模板,继承它,this指针就能变身为shared_ptr。

在这里插入图片描述

8.2 弱回调

把shared_ptr绑(boost::bind)到boost::function里,那么回调的时候Stock-Function始终存在,是安全的。但同时也延长了对象的生命期,使之不短于绑得的boost::function对象。

解决方法:利用weak_ptr,把weak_ptr绑定到boost::function里,这样对象的生命期就不会被延长。在回调的时候先尝试提升为shared_ptr,如果提升成功,说明接受回调的对象还在,则执行回调;若失败,则不执行。

8.3 完整的Stock Factory类代码

class StockFactory : public boost::enable_shared_from_this<StockFactory>, boost::nocopyable {
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>(shred_from_this()), _1));
            //强制把shred_from_this()转型为weak_ptr,以不延长生命期
            //因为boost::bind拷贝的是实参类型,不是形参类型
            wkStock = pStock;
        }
        return pStock;
    }

private:
    static void weakDeleteCallback(const boost::weak_ptr<StockFactory>& wkFactory, Stock* stock) {
        shared_ptr<StockFactory> factory(wkFactory.lock()); //尝试提升
        if(factory) factory->removeStock(stock);    //如果factory还在,清理stocks_
        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_;
};

9 小结

  1. 原始指针暴露给多个线程往往会造成race condition
  2. 统一使用shared_ptr/scoped_ptr来管理对象的生命期,在多线程中尤其重要
  3. 使用shared_ptr需要注意以外延长对象的生命期
  4. weak_ptr可与shared_ptr协作,用作弱回调、对象池等
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值