主旨
用流水线,生产者消费者,任务队列等机制尽量减少跨线程对象的使用。如果一定要用跨线程的对象,那么要注意安全地构造、使用和析构跨线程的对象。对于跨线程对象的使用可以通过mutex保护临界区实现线程安全,对象的生与死不能由对象自身拥有的mutex来保护,本章的重点在于跨线程对象的安全构造和析构问题。
一些基础知识
-
可重入/不可重入函数:在函数调用还没有返回之前,该函数又被调用(比如被中断处理函数调用),处理完中断继续回来执行,函数执行所依赖的环境有没有发生改变。不可重入通常是因为引用了全局变量。
-
mutable:修饰非静态数据成员,在const成员函数中可以被改变
-
线程安全的定义:多个线程同时访问,其表现出正确的行为;无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织,调用端代码无须额外的同步或者其他协调操作。STL大多数class都不是线程安全的
-
空悬指针:指向已经销毁的对象或已经回收的地址;野指针:未经初始化的指针
-
scoped_ptr是boost库中的叫法,在C++11中叫unique_ptr
-
对象的关系:
1)composition组合/复合:x是owner的直接数据成员、scoped_ptr成员或者owner持有的容器的元素,x的生命周期由其owner控制。
2)association关联:a调用b的成员函数,a持有b的指针或者引用,b的生命周期不受a控制。
3)aggregation聚合:从形式上与association一样,但是a和b有逻辑上的整体与部分的关系,但是b可以脱离a独立存在,如车和引擎。
-
fucntion对象和bind
-
enable_shared_from_this: 要使用shared_from_this(), 该对象必须是堆对象,不能在构造函数中使用
构造函数的线程安全
准则:不要在构造函数中泄漏this指针,因为在构造函数中,对象还没有构造完全,这时如果别的线程对这个对象进行访问,访问到的是一个不完整的对象
- 不要在构造函数中注册回调函数
- 不要将this指针传给别的线程的对象
- 即使在构造函数的最后一行也不要泄漏this指针,因为这可能是个基类,后续还要执行子类的构造
解决方法:二段式构造,即构造函数+initialize(),把要传递this指针的操作放到构造函数外面
析构函数的线程安全
不能依靠作为数据成员的mutex保护析构函数
- 析构函数本来就不应该不需要用互斥量去保护,因为只有当别的线程访问不到这个对象时,析构才是安全的。
- 用来保护析构函数的mutex必须是有效的,但是析构函数会把作为成员变量的mutex销毁掉
// 线程不安全的析构函数例子
Foo:~Foo(){
MutexLockGuard lock(mutex_);
...
}
void Foo::update(){
MutexLockGuard lock(mutex_);
...
}
// 线程A
delete x;
x = nullptr; // 没用
// 线程B
if(x){
x->update();
}
竞争条件:把对象的指针暴露给其他函数或者对象之后,如果对象销毁了,并不能通过指针判断出对象是否已经被销毁,这时候通过这个空悬指针访问,就会存在问题。这样的例子有:对于关联和聚合关系,如果b是动态创建的且在程序结束前有可能被释放,那么可能产生竞争条件;注册了任何非静态成员函数的回调,例如Oberver模式。
解决方法:
- 对象池:不存在对象的销毁,但也有很多问题。
- 智能指针:shared_ptr与weak_ptr搭配使用,要注意shared_ptr可能会延长对象的生存期以及循环引用问题。下面两个例子都用到了弱回调,弱回调是指如果对象还活着,就调用它的成员函数,否则忽略之,可以通过尝试将weak_ptr提升为shared_ptr,如果提升成功则表示对象还活着。
Observer模式
问题描述:被观察者保存观察者列表,当事件发生时,通知观察者列表里的每个观察者。这里存在的问题时,如果某个观察者已经析构了,被观察者如果仍然对通知这个观察者,就会出现coredump。本质上是,观察者将this指针传递给了被观察者,如何保障观察者的安全析构。
//Observer.h
class Observer {
public