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++大致会有这么几个方面的问题:
- 缓冲区溢出(buffer overrun)
- 空悬指针/野指针
- 重复释放(double delete)
- 内存泄漏(memory leak)
- 不配对 new[]/delete
- 内存碎片(memory fragmentation)
解决方法:
- 用stl或者自己编写Buffer class来通过接口调用管理缓冲区。
- 用shared_ptr/weak_ptr。
- 用scope_ptr只在对象析构的时候释放一次。
- 同3。
- 把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是多线程、分布式编程的基本功。