本文共 5569 字,大约阅读时间需要 18 分钟。
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的地址,先加锁小的地址。
三种主要对象关系: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++大致会有这么几个方面的问题:
解决方法:
尽量使用shared_ptr/week_ptr替换原始指针(raw pointer),应用于Observer:
class Observable { // not 100% thread safe!public: void register_(const weak_ptr& x); // void unregister(weak_ptr x); void notifyObservers();private: mutable MutexLock mutex_; std::vector > observers_; typedef std::vector >::iterator Iterator;};void Observable::notifyObservers(){ MutexLockGuard lock(mutex_); Iterator it = observers_.begin(); while (it != ovservers_.end()) { shared_ptr 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_ptrglobalPtr;void read(){ shared_ptr localPtr; { MutexLockGuard lock(mutex); localPtr = globalPtr; } doit(localPtr);}
void write(){ shared_ptrnewPtr(new Foo); //在临界区外析构 { MutexLockGuard lock(mutex); globalPtr = newPtr; } doit(newPtr);}
通过shared_ptr local copy + reference to const 传参,可以减小临界区。
如果不小心遗留了shared_ptr的拷贝,那么对象就永远不会被析构。
boost::bind会把实参拷贝一份,使得对象的生命期被延长。
class Foo{ void doit(); };shared_ptrpFoo(new Foo);boost::function func = boost::bind(&foo::doit, pFoo); //Foo在doit执行完才析构
使用const reference的方式传递shared_ptr。
void onMessage(const string& msg){ shared_ptr(new Foo(msg)); //栈上对象,安全 if (validate(pFoo)) { //pass by const reference save(pFoo); //pass by const reference }}
虚析构函数不是必须的,shared_ptr在创建时保存了子类类型指针。
可以定制shared_ptr的析构动作,可传入functor或函数指针等,巧妙结合了泛型编程与面向对象编程。
shared_ptr指向的最后一个对象离开作用域的时候,该对象会在其所在线程析构。
对象的析构可能会导致线程执行时间变长,可以用单独的线程专门做析构。
不懂RAII的C++程序员不是合格的程序员。
通过owner指向child的shared_ptr和child指向owner的weak_ptr来避免循环引用。
一个简单股票对象池(错的):
class StockFactory : boost::noncopyable{ public: shared_ptrget(const string& key);private: mutable MutexLock mutex_; std::map >stock_;}
get从map中获取key,存在即返回否则创建。
问题在于Stock对象永远不会销毁,因为该对象池永远会有一份shared_ptr。
可以将shared_ptr改为weak_ptr来解决:
//std::map>stock_;shared_ptr StockFactory::get(const string& key){ shared_ptr pStock; MutexLockGuard lock(mutex_); weak_ptr & 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, boost::noncopyable{ ... }shared_ptr 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(shared_from_this()), _1));
通常Factory是个singleton,弱回调计数在事件通知中很有用。
学习多线程严禁半桶水上阵,需要严谨系统的训练学习。
学习C++多线程之前必须完整地学习至少一本操作系统的经典著作,《操作系统设计与实现》《现代操作系统》《操作系统概念》。
分析可能出现的race condiction是多线程、分布式编程的基本功。
转载地址:http://mxrr.baihongyu.com/