《C++併發編程實戰》讀書筆記(2):線程間共用數據

来源:https://www.cnblogs.com/moonzzz/archive/2023/09/03/17668362.html
-Advertisement-
Play Games

## 1、使用互斥量 在C++中,我們通過構造`std::mutex`的實例來創建互斥量,調用成員函數`lock()`對其加鎖,調用`unlock()`解鎖。但通常更推薦的做法是使用標準庫提供的類模板`std::lock_guard`,它針對互斥量實現了RAII手法:在構造時給互斥量加鎖,析構時解鎖 ...


1、使用互斥量

在C++中,我們通過構造std::mutex的實例來創建互斥量,調用成員函數lock()對其加鎖,調用unlock()解鎖。但通常更推薦的做法是使用標準庫提供的類模板std::lock_guard<>,它針對互斥量實現了RAII手法:在構造時給互斥量加鎖,析構時解鎖。兩個類都在頭文件<mutex>里聲明。

std::list<int> some_list;
std::mutex some_mutex;

void add_to_list(int value)
{
    //C++17引入了類模板參數推導的新特性,所以下麵語句也可以簡化成:std::lock_guard guard(some_mutex);
    std::lock_guard<std::mutex> guard(some_mutex);
    some_list.push_back(value);
}
bool list_contains(int value)
{
    std::lock_guard<std::mutex> guard(some_mutex);
    return std::find(some_list.begin(), some_list.end(), value) != some_list.end();
}

2、防範死鎖

假設有兩個線程,都需要同時鎖住兩個互斥量才能進行某種操作,但它們分別隻鎖住了一個互斥量,都等著再給另一個互斥量加鎖,這就構成了死鎖。標準庫提供了std::lock函數來解決死鎖的問題,它可以同時鎖住多個互斥量。

class some_big_object {};

void swap(some_big_object& lhs, some_big_object& rhs) {}

class X
{
private:
    some_big_object some_detail;
    mutable std::mutex m;
public:
    X(const some_big_object& sd) :some_detail(sd) {}

    friend void swap(X& lhs, X& rhs)
    {
        if (&lhs == &rhs) { return; }
        std::lock(lhs.m, rhs.m);
        std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
        swap(lhs.some_detail, rhs.some_detail);
    }
};

本例中必須要判斷兩個參數是否指向不同的實例,因為如果已經在某個std::mutex對象上加鎖,那麼再次試圖加鎖將導致未定義的行為。構造std::lock_guard對象時,額外參數std::adopt_lock指明互斥量已被鎖住,std::lock_guard實例應當據此接收鎖的歸屬權,不得在構造函數內試圖另行加鎖。

針對上述場景,C++17還提供了新的RAII模板類std::scoped_lock<>,它和std::lock_guard<>完全等價,只不過前者是可變參數模板,接收各種互斥量型別作為模板參數列表,還以多個互斥量對象作為構造函數參數列表。下列代碼中,傳入構造函數的兩個互斥量都被加鎖,機制與std::lock()函數相同,因此,當構造函數完成時它們都被鎖定,而後在析構函數內一起被解鎖。

void swap(X& lhs, X& rhs)
{
    if (&lhs == &rhs) { return; }
    //這裡使用了C++17的類模板參數推導特性,下麵的語句完全等價於std::scoped_lock<std::mutex, std::mutex> guard(lhs.m, rhs.m);
    std::scoped_lock guard(lhs.m, rhs.m);
    swap(lhs.some_detail, rhs.some_detail);
}

標準庫也提供了std::unique_lock<>模板,它與std::lock_guard<>一樣,也是一個以互斥量作為參數的類模板,並且以RAII手法管理鎖,不過它更靈活一些(代價是略微損失性能)。std::unique_lock<>的構造函數接收第二個參數,我們可以傳入std::adopt_lock以指明std::unique_lock對象管理互斥量上的鎖,也可以傳入std::defer_lock使互斥量在完成構造時處於無鎖狀態,等以後有需要時再加鎖。

void swap(X& lhs, X& rhs)
{
    if (&lhs == &rhs) { return; }
    std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
    std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
    std::lock(lock_a, lock_b);
    swap(lhs.some_detail, rhs.some_detail);
}

std::unique_lock類十分靈活,它具有成員函數lock()try_lock()unlock(),這與互斥量的基本成員函數一致,所以該類可以結合泛型函數來使用,例如std::lock()std::unique_lock的實例可以在銷毀前通過成員函數unlock()解鎖,這意味著如果執行流程的任何特定分支沒有必要繼續持有鎖,那我們就可以提前解鎖,這在有些情況下可能有助於提升程式性能。

鎖的歸屬權可以在多個std::unique_lock實例之間轉移,比如一個函數鎖定互斥量,然後把鎖的歸屬權轉移給函數的調用者,好讓它在同一個鎖的保護下執行其它操作,例如:

std::unique_lock<std::mutex> get_lock()
{
    extern std::mutex some_mutex;
    std::unique_lock<std::mutex> lk(some_mutex);
    prepare_data();
    return lk;
}
void process_data()
{
    std::unique_lock<std::mutex> lk(get_lock());
    do_something();
}

3、保護共用數據的其它工具

3.1、保護共用數據的初始化

假設共用數據只在初始化過程中需要保護,此後無需再進行顯式的同步操作,那麼可以使用std::once_flag類和std::call_once函數來處理這種情況,它們可以保證初始化操作只會執行一次。std::once_flag的實例既不可複製也不可移動,這與std::mutex類似。

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;
void init_resource()
{
    resource_ptr.reset(new some_resource);
}
void foo()
{
    std::call_once(resource_flag, init_resource);
    resource_ptr->do_something();
}

C++11規定了局部靜態變數的初始化只會在某個單一線程上發生,在初始化完成之前,其它線程不會越過靜態數據的聲明而繼續運行。如果某些類只需要用到唯一一個全局實例,這種情況下可以用以下方法代替std::call_once

class my_class;
my_class& get_my_class_instance()
{
    static my_class instance;
    return instance;
}

3.2、保護不常更新的數據

如果我們想要允許單獨一個“寫線程”進行完全排他的訪問,也允許多個“讀線程”共用數據或併發訪問,那麼可以使用C++17提供的新互斥量std::shared_mutex。對於更新操作,使用std::lock_guard<std::shared_mutex>std::unique_lock<std::shared_mutex>鎖定,代替對應的std::mutex特化,它們都保證了訪問的排他性質。對於無需更新數據結構的線程,可以另行改用共用鎖std::shared_lock<std::shared_mutex>,多個線程能夠同時鎖住同一個std::shared_mutex

class dns_entry {};

class dns_cache
{
    std::map<std::string, dns_entry> entries;
    std::shared_mutex entry_mutex;
public:
    dns_entry find_entry(const std::string& domain)
    {
        std::shared_lock<std::shared_mutex> lk(entry_mutex);
        auto it = entries.find(domain);
        return it == entries.end() ? dns_entry() : it->second;
    }
    void update_or_add_entry(const std::string& domain, const dns_entry& dns_details)
    {
        std::lock_guard<std::shared_mutex> lk(entry_mutex);
        entries[domain] = dns_details;
    }
};

3.3、遞歸加鎖

標準庫提供了std::recursive_mutex,其工作方式與std::mutex相似,不同之處是其允許同一線程對某互斥量的同一實例多次加鎖。假如我們對它調用3次lock(),就必須調用3次unlock()才能解鎖。


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

-Advertisement-
Play Games
更多相關文章
  • 分類 懶漢式:實例對象在第一次被使用時才進行初始化。 餓漢式:實例在定義時就被初始化。 特點 1、構造函數和析構函數私有化,不允許外部創建實例對象。 2、拷貝構造函數和複製運算符重載被delete,不允許產生新的實例。 3、內部定義一個私有的靜態數據成員,該成員為本類的實例化對象。 4、提供公有靜態 ...
  • 10分鐘從源碼級別搞懂AQS(AbstractQueuedSynchronizer) ### 前言 上篇文章[15000字、6個代碼案例、5個原理圖讓你徹底搞懂Synchronized](https://juejin.cn/post/7272015112819556412)有說到synchroniz ...
  • 當應用開始脫離單機運行和訪問時,服務發現就誕生了。目前的網路架構是每個主機都有⼀個獨立的 IP 地址,服務發現基本都是通過某種方式獲取到服務所部署的 IP 地址。 DNS 協議是最早將⼀個網路名稱翻譯為網路 IP 的協議,在最初的架構選型中,DNS+LVS+Nginx 基本滿足所有 RESTful ...
  • **條件語句**用於根據不同的條件執行不同的操作。 Go中的條件可以是真或假。 Go支持數學中常見的比較運算符: 小於 大於等於 >= 等於 == 不等於 != 此外,Go還支持常見的邏輯運算符: 邏輯與 && 邏輯或 || 邏輯非 ! 您可以使用這些運算符或它們的組合來創建不同決策的條件。 **示 ...
  • 在實現基於關鍵字的搜索時,首先需要確保MySQL資料庫和ES庫中的數據是同步的。為瞭解決這個問題,可以考慮兩層方案。 1. 全量同步:全量同步是在服務初始化階段將MySQL中的數據與ES庫中的數據進行全量同步。可以在服務啟動時,對ES庫進行全量數據同步操作,以確保數據的一致性。而在停止服務時,可以清 ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《LeetCode952三部曲》系 ...
  • # 實驗目的 - 熟悉並掌握 MIPS 電腦中寄存器堆的原理和設計方法 - 理解源操作數/目的操作數的概念 # 實驗環境 * Vivado 集成開發環境 # MIPS寄存器 ![](https://pic.imgdb.cn/item/64f40fab661c6c8e5400bf9a.jpg) * ...
  • acwing學習筆記,記錄容易忘記的知識點和難題。數組實現單鏈表、雙鏈表、棧、單調棧、隊列、單調隊列、KMP、字典樹 Trie、並查集、數組實現堆、哈希表(拉鏈法、開放定址法、字元串首碼哈希法)、STL常用容器 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...