我們是多麼渴望各種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接口,這個接口的作用是使結點從其所在的鍊中斷開,如圖:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIml2ZucmRudENxUzN5YzN3ITMfBzLchjMvwlNwATMwIzLcRnbl1GajFGd0F2LcRXZu5ibkN3YukGavw1LcpDc0RHaiojIsJye.gif)
它的實作可能是這樣的:
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++在這方面又表現得力不從心,我在這裡引入這個問題旨在于告誡大家在對待多線程問題上一定要細心細心再細心。