本文目录
第1章 线程安全的对象生命期管理
对象在销毁时,出现的竞态条件:
- 析构时,对象的函数被其他线程执行;
- 正在执行对象的函数,如何保证不被其他线程析构;
- 执行对象的函数之前,这个对象是否还活着。
C++标准库的多数类不符合线程安全,比如vector、string等等。
构造不难
对象构造时从以下2点考虑到线程安全:
- 不可以在构造函数中注册回调函数,比如加入观察者。。。。
- 不可以在构造中,把this传给其他线程。
注册回调的方式应该是二段式构造:构造函数+initalize(),如下:
class Foo : public Observer
{
public:
Foo();
void observe(Observer* s)
{
s->register_(this);
}
};
Foo* pFoo = new Foo;
Obervable* s = getSubject();
pFoo->observe(s);//二段式构造
销毁太难
空悬指针(dangling pointer):指向已经销毁的对象。
野指针(wild pointer):未经初始化的指针。
对于普通的成员函数,只需要使用mutex保证临界区不会重叠,就是线程安全的。但是mutex本身不是一直有效,析构会把mutex销毁!对于以下两个函数,使用mutex保护析构:
Foo::~Foo()
{
Lockguard m(mutex_);
//...
}
Foo::update()
{
Lockguard m(mutex_);
//...
}
//thread A
delete x;
x = nullptr;
//thread B
if (x)
{
x->update();
}
以上代码,如果线程A首先执行,线程B阻塞在update调用之前,那么将x赋值为空指针的做法,完全无法避免竞争,线程B触发core dump。以上例子,说明对象销毁之后,把指针置为nullptr完全没用。作为数据成员的mutex不能保护析构。对于基类来说,在进入基类的析构函数之前,派生类的析构已经调用了,说明mutex也无法保护整个析构过程。
此外,mutex可能导致死锁,参考以下代码:
void swap(Foo& a, Foo& b)
{
Lockguard m1(a.mutex_);
Lockguard m2(b.mutex_);
//...
}
//thread A
swap(a, b);
//thread B
swap(b, a);
总结:一个函数要锁住多个相同类型的对象,必须始终按照相同的顺序加锁,实际做法可以按照地址大小顺序加锁。
线程安全的Observer的难点
动态创建的对象是否还活着,看指针和引用是看不出来的。面向对象的程序中,对象之间的关系有3种:
- composition(组合):b是a的成员,不存在竞争关系,是线程安全的;
- aggregation(关联):a用到了b,a持有b的指针或者引用,但是a不能单独控制b的生命期;
- association(聚合):不同于关联的“整体-部分”,聚合更加泛化,可以是一对多、多对一。。。
以观察者模式举例
//观察者
class Observer
{
public:
void update();
void ob(Observable* s)
{
s->register_(this);
subject_ = s;
}
~Oberver()
{
subject_->unregister(this);
}
Observable* subject_;
}
//被观察的
class Observable
{
public:
void register(Observer* s);
void unregister(Observer* s);
void notify()
{
for (Observer* x : observers_)
{
x->update();
}
}
vector<Observer*>