我们是多么渴望各种C++类都是多线程安全的,然而一旦涉及到对象间的交互,这样的渴望可能就只能是奢望了。下面,我们以设计一个双向链结点为例,看看要使其多线程安全将会带来一些什么问题。
class DoublyLinedNode{
DoublyLinedNode* pPrevNode_;
DoublyLinedNode* pNextNode_;
public:
DoublyLinedNode() : pPrevNode_(0), pNextNode_(0){}
virtual ~DoublyLinedNode();
public:
const DoublyLinedNode* GetPrevNode() const{return pPrevNode_;}
const DoublyLinedNode* GetNextNode() const{return pNextNode_;}
public:
void InsertPrevNode(DoublyLinedNode* p);
void InsertNextNode(DoublyLinedNode* p);
void Break();
};
这是一个简单的双向链结点类,我们就讨论讨论其Break接口,这个接口的作用是使结点从其所在的链中断开,如图:
它的实现可能是这样的:
void DoublyLinedNode::Break()
{
if (pPrevNode_)
{
pPrevNode_->pNextNode_ = pNextNode_;
}
if (pNextNode_)
{
pNextNode_->pPrevNode_ = pPrevNode_;
}
pPrevNode_ = 0;
pNextNode_ = 0;
}
这个实现是单线程模式的,没有多线程安全性。
第一次尝试:
void DoublyLinedNode::Break()
{
Lock();
if (pPrevNode_)
{
pPrevNode_->pNextNode_ = pNextNode_;
}
if (pNextNode_)
{
pNextNode_->pPrevNode_ = pPrevNode_;
}
pPrevNode_ = 0;
pNextNode_ = 0;
UnLock();
}
我们第一次尝试将这个接口的代码用多线程锁锁住了,然而问题很明显:
if (pPrevNode_)
{
pPrevNode_->pNextNode_ = pNextNode_;
}
if (pNextNode_)
{
pNextNode_->pPrevNode_ = pPrevNode_;
}
我们这两个对前向和后向结点的操作是修改另外两个对象的内部状态,多线程中,可能在此时正好有其他线程在对这两个对象进行操作(访问),或许程序就会因此而崩溃。
第二次尝试:
void DoublyLinedNode::Break()
{
Lock();
if (pPrevNode_)
{
pPrevNode_->SetNextNode(pNextNode_); // SetNextNode同样添加了锁保护
}
if (pNextNode_)
{
pNextNode_->SetPrevNode(pPrevNode_); // SetPrevNode同样添加了锁保护
}
pPrevNode_ = 0;
pNextNode_ = 0;
UnLock();
}
这第二次尝试将我们对前向和后继结点的内部状态的直接修改改成了对其接口的调用,我们试图通过在其各种接口中加锁来达到多线程安全的目的。然而这却引入了新的问题,我们在一个被锁住的代码中进行了又调用了另外会使用锁的代码,这最可能引发的问题就是资源竞争,而在我们这次尝试中引如的问题的确就是资源竞争,导致死锁:
我们在不同线程中对结点1和结点2同时调用Break,当1申请到自身的锁之后,准备调用2的接口,此时2也申请到了自身的锁,准备调用1的接口。由于1已经占有了自身的锁,2也占有了自身的锁,那么1将会在调用2的接口的地方等待2的锁,而2将会在调用1的接口的地方等待1, 1和2的相互等待就形成了死锁。
第三次尝试:
void DoublyLinedNode::Break()
{
Lock();
if (pPrevNode_)
{
pPrevNode_-> Lock();
pPrevNode_->SetNextNode(pNextNode_);
pPrevNode_-> UnLock ();
}
if (pNextNode_)
{
pNextNode_-> Lock();
pNextNode_->SetPrevNode(pPrevNode_);
pNextNode_-> UnLock ();
}
pPrevNode_ = 0;
pNextNode_ = 0;
UnLock();
}
这次尝试显得比较愚蠢,将外部对象加锁的过程提到了自身Break当中效果和第二次尝试是一样的,没有得到任何的改善。
第四次尝试:
void DoublyLinedNode::Break()
{
SharedLock();
if (pPrevNode_)
{
pPrevNode_->SetNextNode(pNextNode_);
}
if (pNextNode_)
{
pNextNode_->SetPrevNode(pPrevNode_);
}
pPrevNode_ = 0;
pNextNode_ = 0;
SharedUnLock();
}
这次尝试取得了一定的成功,对于这些关系密切,存在相互调用的对象,我们使用了共享锁,它的确将我们的多线程访问冲突和死锁问题解决了,但是这个共享锁的实现难度是相当大的,你必须要保证可能产生相互调用的对象都要进行锁共享,那么你对于增加、修改、删除对象这些管理工作将会变得极度困难,稍有差池就会引发问题,而且别人在使用你的类的时候也同样需要处处小心,这不是我们所期望的。
以上我们进行了四次尝试将我们的双向链结点类设计成多线程安全,显然我们已经筋疲力尽,却未能达到满意的效果。
在这里我建议大家设计这种类的时候尽量设计成单线程模式,在框架设计中去考虑多线程问题,比如使用单线程访问对象,而模块间使用异步通信来进行交互等。
多线程编程的确非常困难,C++在这方面又表现得力不从心,我在这里引入这个问题旨在于告诫大家在对待多线程问题上一定要细心细心再细心。