重读&笔记系列-《Linux多线程服务端编程》第一章

C++《linux多线程服务端编程》笔记


1 线程安全生命期管理

析构函数与多线程

C++多线程的几种竞态条件:

  • 析构时如何保证本对象不正在被执行使用
  • 使用对象时如何保证不正在被析构
  • 使用对象时本对象是否还活着

线程安全class的三个条件:

  • 多线程访问能够正确执行
  • 多线程执行顺序不会影响程序行为
  • 无须额外同步或协调

使用 MutexLock 和 MutexLockGuard 封装 mutex。

线程安全的Counter(部分):

int64_t Counter::value() const
{
	MutexLockGuard lock(mutex_);
  	return value_;
}

int64_t Counter::getAndIncrease()
{
    MutexLockGuard lock(mutex_);
    int64_t ret = value_++;
    return ret;
}

在实际项目中该类使用原子操作效果更好。

对象创建与销毁

构造简单:在构造期间不要泄漏this指针,二段式构造–构造函数+init()。

NO:
Foo(Oberservable *s) {
	s->register_(this);	//非线程安全
}
YES:
Foo();
void oberserve(Oberservable* s) {
    s->regisger_(this);
}
Foo pFoo = new Foo;
Oberservable* s = getSubject();
pFoo->observe(s); //二段式构造

销毁很难:析构函数破坏了mutex互斥锁保护临界区的前提–mutex本身有效。

作为数据成员的mutex无法保护析构–析构函数会销毁mutex。

mutex锁多个对象为了保证不死锁、始终保持顺序性,可以先比较两个mutex的地址,先加锁小的地址。

线程安全的Observer

三种主要对象关系:composition、aggregation、association。

composition(组合)一般不会出多线程问题,各个对象生命期由其唯一的owner控制。

association(关联)、aggregation(聚合)表示a对象持有b对象指针/引用,但不由a管理b的生命期。

association、aggregation其他关系的各个对象生命期复杂,不全部由owner控制。

一个解决方法是使用对象池,不销毁对象。其问题是对象池的线程安全、lock contention、多种对象类型、内存泄漏与分片。

Observer多线程的难点在于,Observable通知每一个Observer之前有可能该对象已经“死”了,无法通过接口判断对象“死活”。

就算Observer在析构中注销自己,会产生更多的race condiction。

原始指针和智能指针

无法通过原始指针来判断对象是否存活,也就是指针指向的地址是否有效无从判断。

解决这个问题可以在指针前引入中间层,通过中间层的引用计数来判断对象是否存活。

这个方案也就是智能指针,c++11已经引入,作为谦卑的程序员直接拿来用就行。

shared_ptr是强引用,引用计数大于0必然不会析构对象,变为0保证析构对象。

weak_ptr不改变引用计数,作为监控来判断对象是否存在。

shared_ptr/weak_ptr使用原子操作计数,安全级别与STL容器一样。

系统避免各种指针错误

C++大致会有这么几个方面的问题:

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

解决方法:

  1. 用stl或者自己编写Buffer class来通过接口调用管理缓冲区。
  2. 用shared_ptr/weak_ptr。
  3. 用scope_ptr只在对象析构的时候释放一次。
  4. 同3。
  5. 把new[]全部替换为vector/scope_array。

尽量使用shared_ptr/week_ptr替换原始指针(raw pointer),应用于Observer:

class Observable { // not 100% thread safe!
public:
    void register_(const weak_ptr<Observer>& x);
    // void unregister(weak_ptr<Observer> x);
    void notifyObservers();
private:
    mutable MutexLock mutex_;
    std::vector<weak_ptr<Obverser>> observers_;
    typedef std::vector<weak_ptr<Obverser>>::iterator Iterator;
};

void Observable::notifyObservers()
{
	MutexLockGuard lock(mutex_);
    Iterator it = observers_.begin();
    while (it != ovservers_.end()) {
        shared_ptr<Observer> obj(it->lock());
        if (obj) {
            obj->update();
            ++it;
        } else {
        	it = observers_.erase(it);
        }
    }
}

该代码有以下问题:

  • 强制要求Observer必须以shared_ptr管理。

  • Observer析构会调用subject_->unregister,需要将Obersevable也由shared_ptr管理。

  • register、unregister、notifyObserver会造成锁争用。

  • 如果update中调用(un)register,如mutex可重入则迭代器失效,否则死锁。

shared_ptr的线程安全

shared_ptr引用计数是线程安全,但其本身不是线程安全的。

一个shared_ptr可被同时读,不可被多个对象同时写(需要加锁),两个shared_ptr可被两个对象同时写(?)。

shared_ptr<Foo> globalPtr;
void read()
{
    shared_ptr<Foo> localPtr;
    {
    	MutexLockGuard lock(mutex);
    	localPtr = globalPtr;
	}
	doit(localPtr);
}
void write()
{
    shared_ptr<Foo> newPtr(new Foo);	//在临界区外析构
    {
    	MutexLockGuard lock(mutex);
    	globalPtr = newPtr;
	}
	doit(newPtr);
}

通过shared_ptr local copy + reference to const 传参,可以减小临界区。

shared_ptr技术与陷阱

意外延长对象的生命期

如果不小心遗留了shared_ptr的拷贝,那么对象就永远不会被析构。

boost::bind会把实参拷贝一份,使得对象的生命期被延长。

class Foo
{
	void doit();  
};
shared_ptr<Foo> pFoo(new Foo);
boost::function<void()> func = boost::bind(&foo::doit, pFoo); //Foo在doit执行完才析构
函数参数

使用const reference的方式传递shared_ptr。

void onMessage(const string& msg)
{
    shared_ptr<Foo>(new Foo(msg));	//栈上对象,安全
    if (validate(pFoo)) {	//pass by const reference
        save(pFoo);			//pass by const reference
    }
}
析构动作在创建时被捕获

虚析构函数不是必须的,shared_ptr在创建时保存了子类类型指针。

可以定制shared_ptr的析构动作,可传入functor或函数指针等,巧妙结合了泛型编程与面向对象编程。

析构所在线程

shared_ptr指向的最后一个对象离开作用域的时候,该对象会在其所在线程析构。

对象的析构可能会导致线程执行时间变长,可以用单独的线程专门做析构。

现成的RAII handle

不懂RAII的C++程序员不是合格的程序员。

通过owner指向child的shared_ptr和child指向owner的weak_ptr来避免循环引用。

对象池

一个简单股票对象池(错的):

class StockFactory : boost::noncopyable
{
public:
    shared_ptr<Stock> get(const string& key);
private:
    mutable MutexLock mutex_;
    std::map<string, stared_ptr<Stock> >stock_;
}

get从map中获取key,存在即返回否则创建。

问题在于Stock对象永远不会销毁,因为该对象池永远会有一份shared_ptr。

可以将shared_ptr改为weak_ptr来解决:

//std::map<string, weak_ptr<Stock> >stock_;
shared_ptr<Stock> StockFactory::get(const string& key)
{
    shared_ptr<Stock> pStock;
    MutexLockGuard lock(mutex_);
    weak_ptr<Stock>& wkStock = stock_[key];
    pStock = wkStock.lock();	//weak_ptr to shared_ptr
    if (!pStock) {
        pStock.reset(new Stock(key));
        wkStock = pStock;	//wkStock是引用
    }
    return pStock;
}

这里的问题在于stock_只增不减,析构stock对象时无法从map中清理,也属于内存泄漏。

可以通过shared_ptr的定制析构功能,在reset的时候传入删除器。

pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, this, _1));
void deleteStock(Stock* stock)
{
    if (stock) {
        MutexLockGuard lock;	//race condition
        stocks.erase(stock->key());
    }
    delete stock;
}

这里存在的问题是调用deleteStock之前,StockFactory有可能已经不存在,导致this指针失效。

解决办法是通过继承enable_shared_from_this,可以返回this指针的shared_ptr 。

class StockFactory : public boost::enable_shared_from_this<StockFactory>, boost::noncopyable
{ ... }
shared_ptr<StockFactory> stockFactory(new StockFactory);
pStock.reset(new Stock(key), boost::bind(&StockFactory::deleteStock, shared_from_this(), _1));

make_shared_from_this 不要在构造函数中使用。

由于返回的是shared_ptr,所以shared_ptr的生命期被延长,可以将其转成weak_ptr进行传递。

在删除器中判断该shared_ptr是否存在,然后进行删除。

pStock.reset(new Stock(key), boost::bind(&StockFactory::weakDeleteCallback, boost::weak_ptr<StockFactory>(shared_from_this()), _1));

通常Factory是个singleton,弱回调计数在事件通知中很有用。

替代方案

心得与小结

学习多线程严禁半桶水上阵,需要严谨系统的训练学习。

学习C++多线程之前必须完整地学习至少一本操作系统的经典著作,《操作系统设计与实现》《现代操作系统》《操作系统概念》。

分析可能出现的race condiction是多线程、分布式编程的基本功。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值