1. 程式人生 > >【VS開發】C++執行緒安全

【VS開發】C++執行緒安全

我們是多麼渴望各種C++類都是多執行緒安全的,然而一旦涉及到物件間的互動,這樣的渴望可能就只能是奢望了。下面,我們以設計一個雙向鏈結點為例,看看要使其多執行緒安全將會帶來一些什麼問題。

class DoublyLinedNode{

       DoublyLinedNodepPrevNode_;

       DoublyLinedNodepNextNode_;

public:

       DoublyLinedNode() : pPrevNode_(0), pNextNode_(0){}

       virtual ~DoublyLinedNode();

public:

       const

 DoublyLinedNodeGetPrevNode() const{return pPrevNode_;}

       const DoublyLinedNodeGetNextNode() const{return pNextNode_;}

public:

       void InsertPrevNode(DoublyLinedNodep);

       void InsertNextNode(DoublyLinedNodep);

       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++在這方面又表現得力不從心,我在這裡引入這個問題旨在於告誡大家在對待多執行緒問題上一定要細心細心再細心。