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

来源:https://www.cnblogs.com/happenlee/archive/2018/10/06/9747743.html
-Advertisement-
Play Games

筆者近期在工作之中編程實現一個Cache結構的封裝,需要使用到C++之中的 互斥量Mutex ,於是花了一些時間進行了調研。( 結果對C++標準庫很是絕望.... )最終還是通過利用了Boost庫的 shared_mutex 解決了問題。借這個機會來聊聊在C++之中的多線程編程的一些 “坑” 。 1 ...


筆者近期在工作之中編程實現一個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++的標準庫實在太簡陋了~~)

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++多線程的相關內容,歡迎大家多多指教。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better tha ...
  • 鏈接:https://www.nowcoder.com/acm/contest/206/B來源:牛客網 題目描述 恬恬有一個nx n的數組。她在用這個數組玩游戲: 開始時,數組中每一個元素都是0。 恬恬會做某些操作。在一次操作中,她可以將某一行的所有元素同時加上一個值,也可以將某一列的所有元素同時加 ...
  • 我先說說feed流吧,它就是社交網站中用戶活動信息流,例如用戶寫了博客、發了照片、評論了什麼等等。Facebook叫newsFeed、推特叫TimeLineFeed。ActivityStream是這些feed規範,它有演員、動作、對象、目標等重要元素組成。用ActivityStream作為信息模型具 ...
  • if語句 * if語句有三種格式 * * if語句格式1: * if(關係表達式){ * 語句體; * } * * 執行順序: * A:首先計算關係表達式的值,看是true還是false * B: 如果是true,執行語句體, * C:如果是false,就不做執行語句體 * * 不影響其它語句的執行 ...
  • 1003 我要通過! (20 分) “ 答案正確 ”是自動判題系統給出的最令人歡喜的回覆。本題屬於 PAT 的“ 答案正確 ”大派送 —— 只要讀入的字元串滿足下列條件, 系統就輸出“ 答案正確 ”,否則輸出“ 答案錯誤”。 得到“ 答案正確 ”的條件是: 字元串中必須僅有 P 、 A 、 T 這三 ...
  • 前面提及過,音頻指紋演算法的思路。 也梳理開源了兩個比較經典的演算法。 https://github.com/cpuimage/shazam https://github.com/cpuimage/AudioFingerprinter 後來一段時間,稍微看了下這兩個演算法,還有不少可以精簡優化的空間。 例 ...
  • 前言 字元串基礎(String) python中字元的定義使用單引號或者雙引號都可以,例如: 註意:在python3中input獲取鍵盤輸入的數據,都以字元串的方式進行保存,即使輸入的是數字。 下標&切片 1.下標 下標:可以理解為數組類數據類型內元素的索引。列表與元組支持下標索引,字元串是字元的數 ...
  • yield表達式用於generator function 調用generator function時,返回一個iterator(函數內語句不被會執行),調用iterator函數時,執行到yield表達式, 當前函數暫停執行,返回表達式的值到調用者,繼續調用iterator函數,從暫停處恢復執行。、 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...