C++11中的mutex, lock, condition variable實現分析

發表於2016-09-21

本文分析的是llvm libc++的實現:http://libcxx.llvm.org/

C++11中的各種mutex, lock物件,實際上都是對posix的mutex,condition的封裝。不過裡面也有很多細節值得學習。

std::mutex

先來看下std::mutex:

包增了一個pthread_mutex_t __m_,很簡單,每個函式該幹嘛就幹嘛。

三種鎖狀態:std::defer_lock, std::try_to_lock, std::adopt_lock

這三個是用於標識鎖在傳遞到一些包裝類時,鎖的狀態:
std::defer_lock,還沒有獲取到鎖
std::try_to_lock,在包裝類構造時,嘗試去獲取鎖
std::adopt_lock,呼叫者已經獲得了鎖
這三個東東,實際上是用於偏特化的,是三個空的struct:
在下面的程式碼裡,就可以看到這三個東東是怎麼用的了。

std::lock_guard

這個類比較重要,因為我們真正使用lock的時候,大部分都是要用這個。

這個類其實很簡單:

在建構函式裡呼叫 mutext.lock(),
在釋構函式裡,呼叫了mutex.unlock() 函式。

因為C++會在函式丟擲異常時,自動呼叫作用域內的變數的解構函式,所以使用std::lock_guard可以在異常時自動釋放鎖,這就是為什麼要避免直接使用mutex的函式,而是要用std::lock_guard的原因了。

注意,std::lock_guard的兩個建構函式,當只傳遞mutex時,會在建構函式時呼叫mutext.lock()來獲得鎖。

當傳遞了adopt_lock_t時,說明呼叫者已經拿到了鎖,所以不再嘗試去獲得鎖。

std::unique_lock

unique_lock實際上也是一個包裝類,起名為unique可能是和std::lock函式區分用的。
注意,多了一個owns_lock函式和release()函式,這兩個在std::lock函式會用到。

owns_lock函式用於判斷是否擁有鎖;

release()函式則放棄了對鎖的關聯,當析構時,不會去unlock鎖。
再看下unique_lock的實現,可以發現,上面的三種型別就是用來做偏特化用的

std::lock和std::try_lock函式

上面的都是類物件,這兩個是函式。

std::lock和std::try_lock函式用於在同時使用多個鎖時,防止死鎖。這個實際上很重要的,因為手寫程式碼來處理多個鎖的同步問題,很容易出錯。

要注意的是std::try_lock函式的返回值:

當成功時,返回-1;

當失敗時,返回第幾個鎖沒有獲取成功,以0開始計數;

首先來看下只有兩個鎖的情況,程式碼雖然看起來比較簡單,但裡面卻有大文章:

上面的lock函式用嘗試的辦法防止了死鎖。

上面是兩個鎖的情況,那麼在多個引數的情況下呢?

先來看下std::try_lock函式的實現:

裡面遞迴地呼叫了try_lock函式自身,如果全部鎖都獲取成功,則依次把所有的unique_lock都release掉。

如果有失敗,則計數失敗的次數,最終返回。

再來看多引數的std::lock的實現:

可以看到多引數的std::lock的實現是:

先獲取一個鎖,然後再呼叫std::try_lock去獲取剩下的鎖,如果失敗了,則下次先獲取上次失敗的鎖。

重複上面的過程,直到成功獲取到所有的鎖。

上面的演算法用比較巧妙的方式實現了引數的輪轉。

std::timed_mutex

std::timed_mutex   是裡面封裝了mutex和condition,這樣就兩個函式可以用:
try_lock_for
try_lock_until

實際上是posix的mutex和condition的包裝。

std::recursive_mutex和std::recursive_timed_mutex

這兩個實際上是std::mutex和std::timed_mutex 的recursive模式的實現,即鎖得獲得者可以重複多次呼叫lock()函式。

和posix mutex裡的recursive mutex是一樣的。

看下std::recursive_mutex的建構函式就知道了。

std::cv_status

這個用來表示condition等待返回的狀態的,和上面的三個表示lock的狀態的用途差不多。

std::condition_variable

包裝了posix condition variable。

裡面的函式都是符合直覺的實現,值得注意的是:
cv_status是通過判斷時間而確定的,如果超時的則返回cv_status::timeout,如果沒有超時,則返回cv_status::no_timeout。

condition_variable::wait_until函式可以傳入一個predicate,即一個使用者自定義的判斷是否符合條件的函式。這個也是很常見的模板程式設計的方法了。

std::condition_variable_any

std::condition_variable_any的介面和std::condition_variable一樣,不同的是std::condition_variable只能使用std::unique_lock,而std::condition_variable_any可以使用任何的鎖物件。

下面來看下為什麼std::condition_variable_any可以使用任意的鎖物件。

可以看到,在std::condition_variable_any裡,用shared_ptr  __mut_來包裝了mutex。所以一切都明白了,回顧std::unique_lock,它包裝了mutex,當析構時自動釋放mutex。在std::condition_variable_any裡,這份工作讓shared_ptr來做了。
因此,也可以很輕鬆得出std::condition_variable_any會比std::condition_variable稍慢的結論了

其它的東東:

sched_yield()函式的man手冊:
sched_yield() causes the calling thread to relinquish the CPU.  The thread is moved to the end of the queue for its
static priority and a new thread gets to run.

在C++14裡還有std::shared_lock和std::shared_timed_mutex,但是libc++裡還沒有對應的實現,因此不做分析。

總結

llvm libc++中的各種mutex, lock, condition variable實際上是封閉了posix裡的對應實現。封裝的技巧和一些細節值得細細推敲學習。

看完了實現原始碼之後,對於如何使用就更加清晰了。

參考:

http://en.cppreference.com/w/cpp

http://libcxx.llvm.org/

相關文章