1. 程式人生 > >自己關於C++多執行緒中物件生命的總結

自己關於C++多執行緒中物件生命的總結

這幾天在寫自己專案的日誌元件,但是遇見了一個很嚴重的問題。就是關於多執行緒中物件生命週期的問題。

問題是這樣的:

首先說明我的日誌元件一共有兩個類:block_queue、log,block queue是用來支援日誌非同步寫日誌的。

在我打算寫一個自己用的日誌元件的時候,我想用到我之前已經封裝好的thread類,以及用mutex和condition類來實現執行緒同步。在我把程式碼大概寫好了之後。日誌庫一執行,就出現:assertion ~mutex() failed, 或者assertion ~condition failed,出現assertion是因為我有習慣對一些易出錯的函式進行assert返回值判斷。這個時候我並不慌亂

因為我憑藉assert已經知道是在它們倆的解構函式中出了問題。然後我開始用GDB除錯,當我一直往下走的時候,走到最後會出現pthread_mutex_destroy.c:not such file or diectoy,或者pthread_cond_destroy.c no such failed。我很奇怪,我的mutex和conditon是利用RAII機制封裝的,按道理來說出現死鎖或者其他問題的概率是很小的,而且這種

no such file or directory.c的錯誤甚至讓我聯想到難道是我缺少這個檔案。但是這不可能,以前一直在用的。然後我用GDB,直接run出現signal終止後,檢視函式呼叫棧,出現最後呼叫就是no such file的呼叫。這個問題很奇怪,我嘗試把assert斷言換成perror,想要列印錯誤號,結果出現的輸出錯誤竟然是.success。

一:條件變數cond析構錯誤

為了摸清到底拿出了問題,於是我用#ifdef _DEBUG_寫了好多列印,用來幫助我檢錯,結果發現有的時候程式一直不能沒有跳出阻塞佇列對隊空時候的迴圈。

阻塞佇列是生產者消費者模型,取任務的功能在take()函式中實現。當佇列中元素為空時,條件變數會一直處於wait狀態。當有隊中放入元素時,會喚醒該條件變數,然後消費者take取任務,開始工作。為了防止驚群效應,在這裡用了while迴圈。

最初的版本是:

  while(queue_.empty()){
        not_empty_.wait();
    }   

但是我當時還處於no such file之中,完全不知道哪裡出了問題。然後一遍又一遍檢視程式碼邏輯,沒有發現問題。並且多執行緒的程式執行邏輯其實是很難用肉眼看清的。為了解決這個問題,我開始在網上搜GDB 除錯多執行緒程式的方法,因為以前程式碼量比較少我沒用GDB除錯過,然後查找了一些資料了列印執行緒資訊,以及切換執行緒的命令,然後開始用GDB進行多執行緒除錯,到訊號終止時,列印函式呼叫棧,切換執行緒,再列印函式呼叫棧,發現一個最後處於wait狀態,一個處於no such file。

這個no such file不明不白,我想到去檢視condition的man手冊,想要在官方文件中找找問題。

  登出一個條件變數需要呼叫pthread_cond_destroy(),只有在沒有執行緒在該條件變數上等待的時候才能登出這個條件變數。

然後我想到了我是在這個cond處於wait狀態時,企圖destroy它,出現了問題,我發現我沒有在解構函式中喚醒該條件變數,然後我在解構函式中果斷加了notify,然後重新測試,結果又出現assertion。

為這個問題糾結了很長時間。

後來發現,如果我呼叫該日誌模組,但是沒有寫入任何日誌的時候,阻塞佇列一直都是空的,那麼它就會一直處於while迴圈之中。為了證實我這個猜想,我在while迴圈中加了列印,發現它果然呼叫了兩次。我就想到一個辦法,既然析構的時候要讓他跳出迴圈,我就在成員變數中加了一個標記,預設為false,析構時為true,這樣就可以在析構的時候也能讓它退出等待。

二:mutex析構錯誤  

  -->mutex_lock_guard是我用RAII機制封裝的一個類,它的內部再建構函式會對建構函式引數mutex進行加鎖,解構函式會對其進行解鎖,在我的block queue中,這個mutex為了共享,它是作為了block queue的成員。

在take函式中,為了保證從佇列中取任務的執行緒安全,所以加鎖保護。

結果還是出現assertion或段錯誤,只不過換成了~mutex返回值錯誤,為這個問題我糾結了一天,因為我認為是在block queue中出現的問題,一直在想block queue中的邏輯是否出現了問題,既然都退出wait狀態了,程式不會阻塞在這裡,為什麼~mutex會出錯呢?

不管是用GDB直接檢視還是用GDB檢視段錯誤core檔案,問題仍然是沒有頭緒。

又糾結了兩天,在網上查詢資料,沒有頭緒。我明明析構的時候已經喚醒了,為什麼還是錯誤呢。

後來我想到了一種可能的情況,就是我的block queue可能出現了“身先死,成員變數未死“,因為我的block queue的成員mutex是利用RAII機制封裝好的現成的類,我的程式碼是這樣的:

template <typename T>
T block_queue<T>::take()
{
/*
    modify: mutex_lock_guard lock(mutex_);
    date  : 2016.11.17
*/
    mutex_lock_guard lock(mutex_); 
    while(queue_.empty() && !is_destroy_){
        not_empty_.wait();
#ifdef _DEBUG_
        std::cout<<"wait"<<std::endl;
#endif
    }   

    if(!queue_.empty()){      //!!!!!!!!!add
        assert(!queue_.empty());

        T front(queue_.front());  //using queue.front to create a T type object
        queue_.pop_front();
    
        return front;
    }   
    return T(0);
}

我猜想,在我程式退出wait迴圈時,我的mutex物件可能還沒有死,block queue就死了。為了證實我的猜想,我在block queue的解構函式中加上了sleep(1)函式來進行延時測試,我發現如果讓block queue函式延時幾秒析構,果然程式沒有錯誤發生。

我又檢查了程式,解構函式已經控制條件變數cond跳出while迴圈,該take()函式完結,那麼mutex類自然就會析構,怎麼會出現”身先死,成員未死呢“?

來回看了好多遍程式,我突然想明白了,take()函式中條件變數類退出迴圈,該函式會朝下執行,但是由於是多執行緒的解構函式也會同時向下執行,那麼問題出來了:

如果該成員函式的執行的速度沒有解構函式執行的速度快,該成員函式還未執行完畢時,解構函式已經執行完畢。然後該成員函式中區域性的mutex類觸發RAII機制,試圖去析構

起始是呼叫pthread_mutex_destroy(mutex_),那麼這時就會出現錯誤,由於引數mutex_也是block queue的成員,此時block queue已經析構掉了,那麼它的成員變數mutex就已經不存在了,這是呼叫一定會出錯的!

然後我明白了這一定是執行緒執行的順序的問題,就查看了一下我的log類,我的log類中有一個block_queue的成員,log類在進行非同步寫日誌時,會建立執行緒呼叫block queue中的成員函式take()函式讀取從佇列中讀取日誌資訊,但是我沒有在log類中加上thread join,問題就出在這裡!

如果我沒有加thread join的話,加入log類析構掉了,沒有等待它創建出的執行緒,那麼log類的成員block queue會先於該執行緒結束前析構掉!

所以我在log類中加入了thread.join()函式之後,問題迎刃而解。

關於這次這個問題的總結:

1.理解了C++多執行緒下的生命週期管理。

2.深刻深刻深刻的體會到了多執行緒程式執行順序的不確定性!

PS:這個問題折磨了我四天!好頭大!