C++霧中風景12:聊聊C++中的Mutex,以及拯救生產力的Boost

筆者近期在工做之中編程實現一個Cache結構的封裝,須要使用到C++之中的互斥量Mutex,因而花了一些時間進行了調研。(結果對C++標準庫非常絕望....)最終仍是經過利用了Boost庫的shared_mutex解決了問題。借這個機會來聊聊在C++之中的多線程編程的一些「坑」面試

1.C++多線程編程的困擾

C++從11開始在標準庫之中引入了線程庫來進行多線程編程,在以前的版本須要依託操做系統自己提供的線程庫來進行多線程的編程。(其實自己就是在標準庫之上對底層的操做系統多線程API統一進行了封裝,筆者本科時進行操做系統實驗是就是使用的pthread或<windows.h>來進行多線程編程的
提供了統一的多線程當然是好事,可是標準庫給的支持實在是有限,具體實踐起來仍是讓人挺困擾的:編程

  • C++自己的STL並非線程安全的。因此缺乏了相似與Java併發庫所提供的一些高性能的線程安全的數據結構。(Doug Lea大神親自操刀完成的併發編程庫,讓JDK5成爲Java之中里程碑式的版本)
  • 若是沒有線程安全的數據結構,退而求其次,能夠本身利用互斥量Mutex來實現。C++的標準庫支持以下的互斥量的實現:
互斥量 版本 做用
mutex C++11 最基本的互斥量
timed_mutex C++11 有超時機制的互斥量
recursive_mutex C++11 可重入的互斥量
recursive_timed_mutex C++11 結合 2,3 特色的互斥量
shared_timed_mutex C++14 具備超時機制的可共享互斥量
shared_mutex C++17 共享的互斥量

由上述表格可見,C++是從14以後的版本才正式支持共享互斥量,也就是實現讀寫鎖的結構。因爲筆者的公司僅支持C++11的版本,因此就沒有辦法使用共享互斥量來實現讀寫鎖了。因此最終筆者只好求助與boost的庫,利用boost提供的讀寫鎖來完成了所需完成的工做。(因此對工具不足時能夠考慮求助於boost庫,確實是解放生產力的大殺器,C++的標準庫實在太簡陋了~~)windows

2.標準庫互斥量的剖析

雖然吐槽了一小節,但並不影響繼續去學習C++標準庫給咱們提供的工具.........(希望公司能再推進升級一波C++的版本~~不過看起來是遙遙無期了)接下來筆者就要來帶領你們簡單剖析一些C++標準庫之中互斥量。安全

mutex

mutex的中文翻譯就是互斥量,不少人喜歡稱之其爲鎖。其實不是太準確,由於多線程編程本質上應該經過互斥量之上加鎖,解鎖的操做,來實現多線程併發執行時對互斥資源線程安全的訪問。 咱們來看看mutex類的使用方法:數據結構

long num = 0;
std::mutex num_mutex;

void numplus() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
    num_mutex.unlock();
};

void numsub() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
    num_mutex.unlock();
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

調用線程從成功調用lock()或try_lock()開始,到unlock()爲止佔有mutex對象。當存在某線程佔有mutex時,全部其餘線程若調用lock則會阻塞,而調用try_lockh會獲得false返回值。由上述代碼能夠看到,經過mutex加鎖的方式,來確保只有單一線程對臨界區的資源進行操做。
time_mutex與recursive_mutex的使用也是大同小異,二者都是基於mutex來實現的。( 本質上是基於recursive_mutex實現的,mutex爲recursive_mutex的特例)
time_mutex則是進行加鎖時能夠設置阻塞的時間,若超過對應時長,則返回false。
recursive_mutex則讓單一線程能夠屢次對同一互斥量加鎖,一樣,解鎖時也須要釋放相同屢次的鎖。
以上三種類型的互斥量都是包裝了操做系統底層的pthread_mutex_t:
pthread_mutex_t結構多線程

在C++之中並不提倡咱們直接對鎖進行操做,由於在lock以後忘記調用unlock很容易形成死鎖。而對臨界資源進行操做時,可能會拋出異常,程序也有可能break,return 甚至 goto,這些狀況都極容易致使unlock沒有被調用。因此C++之中經過RAII來解決這個問題,它提供了一系列的通用管理互斥量的類:併發

互斥量管理 版本 做用
lock_graud C++11 基於做用域的互斥量管理
unique_lock C++11 更加靈活的互斥量管理
shared_lock C++14 共享互斥量的管理
scope_lock C++17 多互斥量避免死鎖的管理

建立互斥量管理對象時,它試圖給給定mutex加鎖。當程序離開互斥量管理對象的做用域時,互斥量管理對象會析構而且並釋放mutex。因此咱們則不須要擔憂程序跳出或產生異常引起的死鎖了。
對於須要加鎖的代碼段,能夠經過{}括起來造成一個做用域。好比上述代碼的栗子,能夠進行以下改寫(推薦):工具

long num = 0;
std::mutex num_mutex;

void numplus() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};
void numsub() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

由上述代碼能夠看到,代碼結構變得更加明晰了,對於鎖的管理也交給了程序自己來進行處理,減小了出錯的可能。性能

shared_mutex

C++14的版本以後提供了共享互斥量,它的區別就在於提供更加細粒度的加鎖操做:lock_sharedlock_shared是一個獲取共享鎖的操做,而lock是一個獲取排他鎖的操做,經過這種方式更加細粒度化鎖的操做。shared_mutex也是基於操做系統底層的讀寫鎖pthread_rwlock_t的封裝:學習

pthread_rwlock_t的結構

這裏有個事情挺奇怪的,C++14提供了shared_timed_mutex 而在C++17提供了shared_mutex。其實shared_timed_mutex涵蓋了shard_mutex的功能。(不知道是否是由於名字被diss了,因此後續在C++17裏將shared_mutex**加了回來)。共享互斥量適用與讀多寫少的場景,舉個栗子:

long num = 0;
std::shared_mutex num_mutex;

// 僅有單個線程能夠寫num的值。
void numplus() {
    std::unique_lock<std::shared_mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};

// 多個線程同時讀num的值。
long numprint() {
    std::shared_lock<std::shared_mutex> lock_guard(num_mutex);
    return num;
}

簡單來講:

  • shared_lock是讀鎖。被鎖後仍容許其餘線程執行一樣被shared_lock的代碼
  • unique_lock是寫鎖。被鎖後不容許其餘線程執行被shared_lock或unique_lock的代碼。它能夠同時限制unique_lock與share_lock

不得不說,C++11沒有將共享互斥量集成進來,在不少讀多寫少的應用場合之中,標準庫自己提供的鎖機制顯得很雞肋,也從而致使了筆者最終只能求助與boost的解決方案。(其實也能夠經過標準庫的mutex來實現一個讀寫鎖,這也是面試筆試之中經常問到的問題。不過太麻煩了,還得考慮和互斥量管理類兼容什麼的,果斷放棄啊)

多鎖競爭

還剩下最後一個要寫的內容:scope_lock ,當咱們要進行多個鎖管理時,很容易出現問題,因爲加鎖的前後順序不一樣致使死鎖。(其實原本不想寫了,好累。這裏就簡單用例子作解釋吧,偷個懶~~)
以下栗子,加鎖順序不當致使死鎖:

std::mutex m1, m2;
// thread 1
{
  std::lock_guard<std::mutex> lock1(m1);
  std::lock_guard<std::mutex> lock2(m2);
}
// thread 2
{
  std::lock_guard<std::mutex> lock2(m2);
  std::lock_guard<std::mutex> lock1(m1);
}

而經過C++17提供的scope_lock就能夠很簡單解決這個問題了:

std::mutex m1, m2;
// thread 1
{
  std::scope_lock lock(m1, m2);
}
// thread 2
{
  std::scope_lock lock(m1, m2);
}

好吧,媽媽不再用擔憂我會死鎖了~~

3.小結

算是簡單的梳理完C++標準庫之中的mutex了,也經過一些栗子比較完整的展示了使用方式。筆者上述關於標準庫的內容,在boost庫之中都能找到對應的實現,不過若是可以使用標準庫,儘可能仍是不要引用boost了。(走投無路的時候記得求助boost,真香~~)但願你們在實踐之中能夠很好的運用好這些C++互斥量來更好的確保線程安全了。後續筆者還會繼續深刻的探討有關C++多線程的相關內容,歡迎你們多多指教。

相關文章
相關標籤/搜索