C++多線程面向對象解決方案

相信很多人都讀過《C++沉思錄》這本經典著作,在我艱難地讀完整本書後,留給我印象最深的只有一句話::“用類表示概念,用類解決問題”。

關 於多線程編程,如果不是特別需要,大多數開發人員都不會特意去觸碰這個似乎神祕的領域。如果在某些場合能正確並靈活地運用,多線程帶來的好處是不言而喻 的。然而,任何事物都有兩面性,如果程序中引入多線程,那麼我們需要謹慎小心地處理許多與之相關的問題,其中最突出的就是:資源競爭、死鎖和無限延遲。那 麼面向對象與這些有什麼關係了嗎?有,面向對象的基礎是封裝,是的,正是封裝,可以很好的解決多線程環境中的主要困境。

一.多線程環境

在開始之前,有必要先重溫一下多線程環境。所謂,多線程編程,指的是在一個進程中有多個線程同時運行,每一個線程都有自己的堆棧,但是這些線程共享所有的全局變量和資源。

在引入互斥量之前,多線程的主要問題是資源競爭,所謂資源競爭就是兩個或兩個以上的線程在同一時間訪問同一資源。爲了解決這個問題,我們引入了互斥量,以同步多個線程對同一資源的訪問。

然 而在引入互斥量之後,新的問題又來了。因爲如果互斥量的獲得和釋放沒有得到正確處理,就會引起嚴重的問題。比如,某線程獲得互斥量後,就可以對受保護的資 源進行訪問,但是如果訪問完畢後,忘記了釋放互斥量,那麼其它的線程永遠也無法訪問那個受保護的資源了。這是一種較簡單的情況,還有一種複雜的,那就是線 程1已經擁有了資源A,但是它要擁有資源B後才能釋放A,而線程2了恰好相反,線程2已經擁有了資源B但是它要擁有資源A後才能釋放B。這樣一來,線程1和線程2就永遠地相互等待,這就是所謂的死鎖。

死 鎖導致的問題是嚴重的,因爲它使得程序無法正常運行下去。也許引入一些規範或約束有助於減少死鎖發生的機率,比如我們可以要求,所有資源的訪客(客戶,使 用者)都必須在真正需要資源的時刻請求互斥量,而當資源一使用完畢,就立即釋放它,另外,鎖定與釋放一定要是成對的。如果上面的線程1和線程2都遵守這個規範,那麼上述的那種死鎖情況就不會發生了。

然而,規範永遠只是規範,規範能被執行多少要依賴於使用者的自覺程度有多高,這個世界上總是有對規範和約束視而不見的人存在。所以,我們希望能夠強制執行類似的約束,在對使用者透明的情況下。對於約束的強制實施可以通過封裝做到。

 

二.多線程面向對象解決方案

首先你需要將系統API封裝成基礎類,這樣你就可以用面向對象的武器類對付多線程環境,二是將臨界資源與對其的操作封裝在一個類中。這兩點的核心都是將問題集中在一個地方,防止它們氾濫在程序的各個地方。

 1.  將系統API封裝成基礎類。

 1.  將系統API封裝成基礎類。

厭倦了每次涉及共享資源操作時都需要調用InitializeCriticalSection、DeleteCriticalSection、EnterCriticalSection、LeaveCriticalSection,並且它們是成對使用的,如果你調用了EnterCriticalSection,卻忘了調用LeaveCriticalSection,那麼鎖就永遠得不到釋放,並且這些API的使用是很不直觀的。我喜歡將它們封裝成類,只需封裝一次,以後就不用再查MSDN,每個API怎麼寫的了,參數是什麼,免去後顧之憂。而且,在類的構造函數中調用InitializeCriticalSection,析構函數中調用DeleteCriticalSection,可以防止資源泄漏。面向對象的封裝真是個好東西,我們沒有理由拒絕它。

來看看我封裝的幾個與多線程環境相關的基礎類。

// CriticalSection類用於解決對臨界資源的保護

class CriticalSection

{

protected:

       CRITICAL_SECTION critical_section ;

public:

       CriticalSection()

       {

              InitializeCriticalSection(&this->critical_section) ;

       }

       virtual ~CriticalSection()

       {

              DeleteCriticalSection(&this->critical_section) ;

       }

       void Lock()

       {

              EnterCriticalSection(&this->critical_section) ;

       }

       void Unlock()

       {

              LeaveCriticalSection(&this->critical_section) ;

       }

}; 

//Monitor用於解決線程之間的同步依賴

class Monitor

{

private:

HANDLE event_obj ;

public:

Monitor(BOOL isManual = FALSE)

{

        this->event_obj = CreateEvent(NULL ,FALSE ,isManual ,"NONAME") ;

}

~Monitor()

{

        //ReleaseEvent()

        CloseHandle(this->event_obj) ;

void SetIt()

{

        // 如果爲auto,則SetEvent將event obj設爲有信號,當一個等待線程release後,

        //event obj自動設爲無信號

        //如果是manual,則release所有等待線程,且沒有後面自動重設

        SetEvent(this->event_obj) ;

}

void ResetIt()

{    

        //手動將event obj設爲無信號

        ResetEvent(this->event_obj) ;

void PulseIt()

{

        // 如果爲auto,則PulseEvent將event obj設爲有信號,當一個等待線程release後,

        //event obj自動設爲無信號

        //如果是manual,PulseEvent將event obj設爲有信號,且release所有等待線程,

        //然後將event obj自動設爲無信號

        PulseEvent(this->event_obj) ;

DWORD Wait(long timeout)

{

        return WaitForSingleObject(this->event_obj ,timeout) ;

}

}; 

//Thread是對線程的簡單封裝

class Thread

{

private:    

HANDLE threadHandle ; 

unsigned long  threadId ;    

    unsigned long  exitCode ;

BOOL needTerminate ;

 public:

 public:

Thread(unsigned long exit_code = 0 )

{           

        this->exitCode = exit_code ;

        this->needTerminate = FALSE ;

}

 ~Thread(void)

 ~Thread(void)

{

        if(this->needTerminate)

        {           

               TerminateThread(this->threadHandle ,this->exitCode) ;

        }

long GetTheThreadID()

{

        return this->threadId ;

void Start(FunPtr pfn ,void* pPara)//啓動線程

{

        this->threadHandle = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(pfn) ,pPara ,0,&(this->threadId));  

}

void SetTerminateSymbol(BOOL need_Terminate)

{

        this->needTerminate = need_Terminate ;

void wait(void)

{

        WaitForSingleObject(this->threadHandle,INFINITE) ; //用於阻塞宿主線程,使其不能早於本線程結束

}

}; 

        在大多數的多線程環境中,上述的幾個類已經夠用了,不如要實現更強勁的同步機制,你可以仿照上面自己進行封裝。

2.  將臨界資源與對其的操作封裝在一個類中,如果這樣做,鎖的操作自動在類的實現中完成,而外部使用者不用關心是否處在多線程環境。也就是說這個類是線程安全的,在單線程和多線程環境下都可以使用。

比如我們經常需要使用線程安全的容器,我就自己封裝了一個:

// SafeObjectList 線程安全的容器

#include <list>

#include "../Threading/CriticalSection.h"

 

plate<class T> class SafeObjectList : CriticalSection

{

private:

list<T> inner_list ;

list<T>::iterator itr ;

public:

void Add(T obj)

{

        this->Lock() ;

        this->inner_list.push_back(obj) ;

        this->Unlock() ;

}

void Remove(T obj)

{

       

        this->Lock() ;

        for(this->itr = this->inner_list.begin() ;this->itr != this->inner_list.end() ;this->itr++)

        {

               if(obj == (*(this->itr)))

               {

                      this->inner_list.erase(this->itr) ;

                      break ;

               }

        }

        this->Unlock() ;

       

void Clear()

{

        this->Lock() ;

        this->inner_list.clear() ;

        this->Unlock() ;

}

int Count()

{

        return (int)this->inner_list.size() ;

BOOL Contains(T& target)

{

        BOOL found = FALSE ; 

        this->Lock() ;

        for(this->itr = this->inner_list.begin() ;this->itr != this->inner_list.end() ;this->itr++)

        {

               if(target == (*(this->itr)))

               {

                      found = TRUE ;

                      break ;

               }

        }

        this->Unlock() ; 

        return found ; 

BOOL GetElement(int index ,T& result)

{

        BOOL succeed = FALSE  ;

        this->Lock() ;       

        if(index < (int)this->inner_list.size())

        {

               int i= 0 ;

               for(this->itr = this->inner_list.begin() ;this->itr != this->inner_list.end() ;this->itr++)

               {

                      if(i == index)

                      {

                             result =  (*this->itr) ;

                             break ;

                      }

                      i++ ;

               }    

              

               succeed = TRUE ;

        }

        this->Unlock() ;

       

        return succeed ;           

}    

};

 

       在 將臨界資源與對其的操作封裝在一個類中的時候,我們特別要需要注意的一點是封裝的線程安全的方法(函數)的粒度,粒度太大則難以複用,粒度太小,則可能會 導致鎖的嵌套。所以在封裝的時候,一定要根據你的具體應用,視情況而定。我的經驗是這樣的,首先可以把粒度定小一點,但是一旦發現有鎖的嵌套出現,就加大 粒度,把這兩個嵌套合併成一個稍微粗一點粒度的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章