《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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...