Linux(muduo網絡庫):04---線程同步概要之(封裝MutexLock、MutexLockGuard、Condition)

  • 本文內容銜接於前一篇文章(互斥器、條件變量、讀寫鎖、信號量、sleep)https://blog.csdn.net/qq_41453285/article/details/104859230
  • MutexLock、MutexLockGuard、Condition等class完整代碼在muduo/base中都可以找到,這幾個class都不允許拷貝構造和賦值

一、MutexLock、MutexLockGuard的封裝

  • MutexLock:封裝臨界區(critical section),這是一個簡單的資源類,用RAII手法封裝互斥器的創建與銷燬。MutexLock一般是別的class的數據成員
    • 臨界區在Windows上是struct CRITICAL_SECTION,是可重入的
    • 在Linux下是pthread_mutex_t,默認是不可重入的
  • MutexLockGuard:封裝臨界區的進入和退出,即加鎖和解鎖。 MutexLockGuard一般是個棧上對象,它的作用域剛好等於臨界區域

MutexLock

  • 說明:
    • MutexLock的附加值在於其提供了isLockedByThisThread()函數,用於程序斷言
    • 關於tid()函數,在後面文章我們還會詳細介紹“Linux的線程標識”
  • 代碼如下:
class MutexLock :boost::noncopyable
{
public:
    MutexLock() :holder_(0)
    {
        pthread_mutex_init(&mutex_, NULL);
    }

    ~MutexLock()
    {
        assert(holder_ == 0);
        pthread_mutex_destory(&mutex_);
    }

    bool isLockByThisThread()
    {
        return holder_ == CurrentThread::tid();
    }

    void assertLocked()
    {
        assert(isLockByThisThread());
    }

    //僅供MutexLockGuard調用,嚴禁用戶代碼調用
    void lock()
    {
        //這兩行順序不能反
        pthread_mutex_lock(&mutex_);
        holder_ = CurrentThread::tid();
    }
    //僅供MutexLockGuard調用,嚴禁用戶代碼調用
    void unlock()
    {
        holder_ = 0;
        pthread_mutex_unlock(&mutex_);
    }

    //僅供Condition調用,嚴禁用戶代碼調用
    pthread_mutex_t* getPthreadMutex()
    {
        return &mutex_;
    }
private:
    pthread_mutex_t mutex_;
    pid_t holder_;
};

MutexLockGuard

  • 代碼如下:
class MutexLockGuard :boost::noncopyable
{
public:
    explicit MutexLockGuard(MutexLock& mutex):mutex_(mutex)
    {
        mutex_.lock();
    }

    ~MutexLockGuard()
    {
        mutex_.unlock();
    }
private:
    MutexLock& mutex_;
};

#define MutexLockGuard(x) static_assert(false,"missing mutex guard var name");
  •  注意上面代碼的最後一行定義了一個宏,這個宏是爲了防止程序裏出現下面這樣的錯誤:
void doit()
{
    //錯誤,產生一個臨時對象,互斥器創建之後立馬又銷燬了,下面的臨界區沒有鎖住
    MutexLockGuard(mutex);
    //正確的做法要加上變量名,例如:MutexLockGuard lock(mutex)
    
    //...臨界區
}
  • 注意事項:
    • 有人把MutexLockGuard寫成template,此處沒有這麼做是因爲它的模板類型參數只有MutexLock一種可能,沒有必要隨意增加靈活性,於是我手工把模板具現化(instantiate)了
    • 此外一種更激進的寫法是,把lock/unlock放到private區,然後把MutexLockGuard設爲MutexLock的friend。我認爲在註釋裏告知程序員即可,另外check-in之前的code review也很容易發現誤用的情況(grep getPthreadMutex)
  • 這段代碼沒有達到工業強度:
    • mutex創建爲PTHREAD_MUTEX_DEFAULT類型,而不是我們預想的PTHREAD_MUTEX_NORMAL類型(實際上這二者很可能是等同的),嚴格的做法是用mutexattr來顯示指定mutex的類型(互斥量屬性可以參閱:https://blog.csdn.net/qq_41453285/article/details/90904833
    • 沒有檢查返回值。這裏不能用assert()檢查返回值,因爲assert()在release build裏是空語句。我們檢查返回值的意義在於防止ENOMEM之類的資源不足情況,這一般只可能在負載很重的產品程序中出現。一旦出現這種錯誤,程序必須立刻清理現場並主動退出,否則會莫名其妙地崩潰,給事後調查造成困難。這裏我們需要non-debug的assert,或許google-glog的CHECK()宏是個不錯的思路
  • 一些其他想法:
    • muduo庫的一個特點是隻提供最常用、最基本的功能,特別有意避免提供多種功能近似的選擇。muduo不是“雜貨鋪”,不會不分青紅皁白地把各種有用的、沒用的功能全鋪開擺出來。muduo刪繁就簡,舉重若輕;減少選擇餘地,生活更簡單
    • MutexLock沒有提供trylock()函數,因爲我沒有在生成代碼中用過它。我想不出什麼時候程序需要“試着去鎖 一鎖”,或許我寫過的代碼太簡單了(trylock的一個用途是用來觀察lock contention,見[RWC]“Consider using nonblocking synchronization routines to monitor contention”)

二、Condition的封裝

  • 條件變量(condition variable)允許在 wait()的時候指定mutex
  • 關於爲什麼要自己封裝Condition這個類:
    • 但是我想不出有什麼理由一個condition variable會和不同的mutex配合使用。Java的intrinsic condition和Condition class都不支持這麼做,因此我覺得可以放棄這一靈活性,老老實實地一對一好了
    • 相反,boost::thread的condition_variable是在wait的時候指定mutex, 請參觀其同步原語的龐雜設計:
      • Concept有四種:Lockable、TimedLockable、SharedLockable、 UpgradeLockable
      • Lock有六種:lock_guard、unique_lock、shared_lock、 upgrade_lock、upgrade_to_unique_lock、scoped_try_lock
      • Mutex有七種:mutex、try_mutex、timed_mutex、 recursive_mutex、recursive_try_mutex、recursive_timed_mutex、 shared_mutex
    • 恕我愚鈍,見到boost::thread這樣小題大做的庫,我只得三揖繞道而行。很不幸C++11的線程庫也採納了這套方案。這些class名字也很無厘頭,爲什麼不老老實實用readers_writer_lock這樣的通俗名字呢?非得增加精神負擔,自己發明新名字。我不願爲這樣的靈活性付出代價,寧願自己做幾個簡簡單單的一看就明白的class來用,這種簡單的幾行代碼的“輪子”造造也無妨。提供靈活性固然是本事,然而在不需要靈活性的地方把代碼寫死,更需要大智慧

Condition

  • 下面這個muduo::Condition class簡單地封裝了條件變量,用起來也容易
  • 關於成員函數的命名規則:
    • 這裏用notify/notifyAll作爲函數名,因爲signal有別的含義,C++裏的signal/slot、C裏的signalhandler等等
    • 就以爲了不產生衝突,我們自己定義了這些成員函數的名稱
class Condition :boost::noncopyable
{
public:
    explicit Condition(MutexLock& mutex) :mutex_(mutex)
    {
        pthread_cond_init(&pcond_);
    }
	~Condition()
    {
        pthread_cond_destory(&pcond_);
    }

    void wait()
    {
        pthread_cond_wait(&pcond_, mutex_.getPthreadMutex());
    }
    void notify()
    {
        pthread_cond_signal(&pcond_);
    }
    void notifyAll()
    {
        pthread_cond_broadcast(&pcond_);
    }
private:
    MutexLock& mutex_;
    pthread_cond_t pcond_;
};

關於條件變量與互斥器的使用

  • 如果一個類要包含MutexLock和Condition,一定要注意它們的聲明順序和初始化順序:
    • MutexLock應該先於Condition構造
    • 並且MutexLock用來初始化Condition
  • 例如下面一個CountDownLatch(倒計時)class:
class CountDownLatch
{
public:
    //構造函數中,初始化順序要與聲明順序一致
    //並且使用mutex_初始化conditon_
    CountDownLatch(MutexLock& mutex)
        :mutex_(mutex), conditon_(mutex_), count_(0) {}
private:
    MutexLock& mutex_;   //互斥器先於條件變量定義
    Condition conditon_;
    int count_;
};

三、總結

  • 請允許我再次強調,雖然本章花了大量篇幅介紹如何正確使用mutex和condition variable,但並不代表我鼓勵到處使用它們:
    • 這兩者都是非常底層的同步原語,主要用來實現更高級的併發編程工具
    • 一個多線程程序裏如果大量使用mutex和condition variable來同步,基本跟用鉛筆刀鋸大樹(孟巖語)沒啥區別
  • 在程序裏使用Pthreads庫有一個額外的好處:分析工具認得它們, 懂得其語意。線程分析工具如Intel Thread Checker和Valgrind-Helgrind 33 等能識別Pthreads調用,並依據happens-before關係(參閱:http://research.microsoft.com/en-us/um/people/lamport/pubs/time-clocks.pdf)分析程序有無data race

四、附加

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