一、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
四、附加