文章目录
线程安全的对象生命期管理
1.1析构函数遇到多线程
当一个对象能被多个线程同时看到时,那么对象的销毁时机就会变得模糊不清,可能出现多种竞态条件。
- 在即将析构一个对象时,从何而知此可是否有别的线程正在执行该对象的成员函数?
- 如何保证在执行成员函数期间,对象不会在另一个线程被析构?
- 在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会碰巧执行到一半?
1.1.1线程安全的定义
一个线程安全的类应该满足以下三个条件
- 多个线程同时访问时,其表现出正确的行为。
- 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织。
- 调用端代码无须额外的同步或其他协调动作。
依据这个定义,C++标准库里的大多数class
都不是线程安全的,包括std::string
,std::vector
,std::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
。
- 线程A执行到了析构函数的(1)处,已经持有了互斥锁,即将继续往下执行。
- 线程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_
指向某个永久存在的对象,那么还是会出问题:
- 线程A执行到了12行之前,还没有来得及
unregister
本对象。 - 线程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里被释放这种错误。- 析构动作可以定制。
析构所在的线程:对象的析构是同步的,当最后一个指向x
的shared_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)
,其中ptr
是shared_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
}
这样,无论Stock
和StockFactory
谁先挂掉都不会影响程序的正确运行。