目录
1 MutexLock与MutexLockGuard
MutexLock
:封装临界区,这是一个简单的资源类,用RAII手法封装互斥器的创建与销毁
MutexLockGuard
:封装临界区的进入和退出,即加锁和解锁。MutexLockGuard一般是个栈上对象,它的作用域刚好等于临界区域
这两个类都不允许拷贝构造与赋值
2 空悬指针
解决方法一:引入一层间接性
更好的办法:引入引用计数
3 智能指针
3.1 shared_ptr
当引用计数降为0时,对象(资源)即被销毁
3.2 weak_ptr
不增加对象的引用次数,弱引用
3.3 关键点
4 C++可能出现的内存问题
4.1 类型
- 缓冲区溢出(buffer overrun)
- 空悬指针/野指针
- 重复释放(double delete)
- 内存泄漏(memory leak)
- 不配对的new[]/delete
- 内存碎片(memory fragmentation)
4.2 解决思路
- 缓冲区溢出:用
std:vector<char>/std:string
或自己编写Buffer class来管理缓冲区,自动记住用缓冲区的长度,并通过成员函数而不是裸指针来修改缓冲区。 - 空悬指针/野指针:用shared_ptr/weak_ptr。
- 重复释放:用scoped_ptr,只在对象析构的时候释放一次。
- 内存泄漏:用scoped_ptr,对象析构的时候自动释放内存。
- 不配对的new[]/delete:把new[]统统替换为
std::vector/scoped_array
。 - 内存碎片
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如何subject_还活着
-
即使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 意外延长对象的生命期
-
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); //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 小结
- 原始指针暴露给多个线程往往会造成race condition
- 统一使用shared_ptr/scoped_ptr来管理对象的生命期,在多线程中尤其重要
- 使用shared_ptr需要注意以外延长对象的生命期
- weak_ptr可与shared_ptr协作,用作弱回调、对象池等