5.构造和析构的线程安全
(1).构造函数的线程安全
规则:不要在构造函数中注册任何回调函数;
不要在构造函数中把this传递给跨越线程的对象。
使用二段式构造(构造函数+Initialize函数)
ex:这里有问题的原因是,可能在构造函数没有执行完成之前,Obs就去调用类A的函数了。
class A
{
public:
A(Obs *pObs)
{
pObs->Obeserve(this);
}
};
ex:解决方法是二段式构造
class A
{
public:
A(Obs *pObs)
{
...
}
Initialize()
{
//在Initialize里面干些线程安全的事情
}
};
(2).析构的线程安全
.问题的提出:
当一个对象被多个线程访问的时候,
如下代码可能有两个问题:一个线程可能正在访问一个被释放了的变量pX。
而且mutex也是一个函数成员变量,在~A()析构函数执行了之后mutex就被释放了。
ex:
先线程1中执行了下面的代码:
~A()
{
mutex_lock();
delete pX;
...
}
但是另外一个线程2,却要访问变量 px:
void A::getX()
{
mutex_lock();
pX->...
mutex_unlock();
}
2013年11月12日:
1.observer模式的线程安全
(1).observer的一般模式是:
class Observer
{
public:
virtual ~Observer()
{
}
virtual void update() =0 ;
};
class Observable
{
public:
void register(Observer* x);
void unregister(Observer* x);
void notifyObservers()
{
foreach Observer* x
{
// 这行是伪代码
x->update(); //(3)
}
}
~Observable()
{
unregister 同时delete 所有的observer指针
}
//
...
}
(2).分析多线程中observer模式可能出现的问题
在标注(3)(x->update)上有可能出现的问题是:如何得知 observer对象 x 还活着? 因为有可能线程A在执行 ~Observable()的时候,
线程B已经进入到 x->update()里面, 这个问题就非常危险了。
也行你会这样修改observer:但是这里仍然有问题,如何确保_observable对象存在?并且如何确保 在执行标注(3)的同时,没有
另外一个线程在执行标注(4);
class Observer
{
public:
virtual ~Observer()
{
_observable->unregister(...) //(4)
}
virtual void update() =0 ;
Observable *_observable ;
};
上面的问题让你焦头烂额??
也行你会用 线程的同步来解决这些问题。但是这很有可能让你的线程失去并发特性,而变成按照一定顺序来执行线程了。这
对程序性能明显有很大的影响。
怎样解决??
通过分析上面的问题,我的总结这里的场景可以归结为:一个线程在调用对象的功能,但是很有可能同时有另外一个线程在
调用同一个对象的析构函数或者这个析构函数已经执行完成,另一个线程却不知。
1.1怎样解决上面场景中出现的问题?
我们的解决方法:使用weak_ptr和shared_ptr
ex:
class Observable //not100% thread safe!
{
public:
void register(weak_ptr<Observer> x);
void unregister(weak_ptr<Observer> x);
//可用std::remove/vector::erase实现
void notifyObservers()
{
MutexLock lock(mutex_);
Iterator it= observers_.begin();
while(it!=observers_.end())
{
shared_ptr<Observer> obj(it->lock());//尝试提升,这一步是线程安全的
if(obj)
{
//提升成功,现在引用计数值至少为2(想想为什么?)
obj->update();//没有竞态条件,因为obj在栈上,对象不可能在本作用域内销毁
++it;
}
else
{
//对象已经销毁,从容器中拿掉
weak_ptr it=observers_.erase(it);
}
}
}
private:
std::vector<weak_ptr<Observer>> observers_;//(5)
mutable Mutex mutex_;
};
思考:如果把(5)处改为vector<shared_ptr<Observer>>observers_;,会有什么后果? 后果就是 Observer 永远活着,知道调用 vector的析构
函数。但是我们这里用weak_ptr,是因为我们保证对象在某个地方活着。
需要注意循环引用:通常是 owner持有A的shared_ptr, A 持有 owner的 weak_ptr.
2.RAII
RAII(Resource Acquisition Is Initialization),中文的翻译是:资源获取就是初始化。
背景:系统的资源是有限的,怎样正确的做到资源的使用和释放就是非常重要的编程方法。
ex:
void A::f()
{
FILE *pFile = fopen(...);
...
thr();
if(..)
{
return ...;
}
fclose(pFile);
}
上面的代码是肯定有问题的,因为如果直接就return的话,文件就没有关闭。但是如果我们中间有很多的if语句,
并且thr()还有可能会跑出异常,那么我们怎样正确的 fclose(pFile) 呢?
如果我们到处添加 fclose(pFile),那么程序就会很难管理。
解决方法1: 在类 A 的析构函数中调用 fclose(pFile)
解决方法2: 使用shared_ptr,或者scoped_ptr 能很好的解决问题。
RAII就是解决 资源关闭的问题的。
这种思想本质是在析构函数中处理。因为当函数返回的时候会调用析构函数;并且当抛出异常的时候,系统会沿着调用栈,
寻找catch子句,这个过程stack unwinding。c++规定,在stack unwinding这个过程中,系统必须确保调用所有以创建的局部对象
的析构函数。
2.原子锁的利用
我们有时需要在创建一个类的第一个对象的时候,做某种初始化,并且只初始化一次。
在调用这个类的最后一个对象的析构函数的时候,做某种析构动作,而且只析构一次。
这里就到了 interlockadd大显身手的时候了:
ex:
class A
{
A()
{
InterlockedInc64(m_instanceNum);
if (m_instanceNum==1)
{
dosomething...
}
}
~A()
{
InterlockedDecrement64(m_instanceNum);
if (m_instanceNum==1)
{
dosomething...
}
}
long m_instanceNum;
};