讀寫鎖 shared_mutex 共享互斥量介紹

文章目錄

    • 讀數據對數據沒有影響,為什么還需要shared_mutex
      • 1. 保證讀取數據的“一致性”和“時效性”
      • 2. 協調“讀”與“寫”的競爭關系
      • 總結
    • 好的,我們來詳細介紹 C++17 中的 `std::shared_mutex`(共享互斥量,俗稱讀寫鎖)的使用。
      • 核心概念
      • 包含頭文件
      • 基本使用步驟
        • 1. 定義共享數據和共享互斥量
        • 2. 讀操作 - 使用 `std::shared_lock`
        • 3. 寫操作 - 使用 `std::unique_lock` 或 `std::lock_guard`
      • 完整示例代碼
      • 可能輸出及分析:
      • 重要注意事項

讀數據對數據沒有影響,為什么還需要shared_mutex

這是一個非常經典且重要的問題。您的直覺是正確的——單純的讀操作本身確實不會改變數據。但關鍵在于,我們使用 std::shared_mutex(共享鎖/讀寫鎖)的目的,不僅僅是為了防止讀操作“搞破壞”,更是為了保護讀操作自身能獲得一個正確、可靠的結果

核心原因在于:并發編程的世界里,您不能只考慮一個線程的行為,必須考慮多個線程同時操作同一份數據時可能發生的交互和沖突。

讓我們用一個比喻來理解:

想象一下,您(讀線程)正在閱讀一本非常重要的參考書(數據)。

  • 沒有鎖的場景:當您正在閱讀第100頁時,圖書管理員(寫線程)突然過來把第100頁撕掉,換成了新的一頁。您讀到的內容就變成了半句舊話和半句新話的混合體,這顯然是錯誤且無意義的。這就是臟讀(Dirty Read)
  • 使用 std::mutex 的場景:為了保護書的內容,圖書館規定一次只允許一個人進入(獨占鎖)。無論您是去閱讀(讀)還是去修改(寫),都要排隊。這非常安全,但效率極低,因為明明可以允許多個人同時閱讀。
  • 使用 std::shared_mutex 的場景:圖書館現在有了新規則:允許多個人同時閱讀(共享鎖),但只要有人需要修改書籍(寫線程申請獨占鎖),就會阻止新的讀者進入,并等待所有現有的讀者離開后,才進行修改。修改完成后,再允許新的讀者進入。這既保證了效率(多人同時讀),又保證了安全(讀的時候書不會變,寫的時候是獨占的)。

從技術角度,主要有以下兩個問題需要解決:


1. 保證讀取數據的“一致性”和“時效性”

即使讀操作不修改數據,它也需要讀到某個特定時間點的、完整一致的數據。

  • 問題一:臟讀 (Dirty Read)

    • 場景:寫線程B開始修改數據(例如,分兩步更新一個結構體),剛更新到一半。
    • 此時:讀線程A來讀取這個數據。它讀到的是一半新、一半舊的中間狀態,這數據是無效的、從未正式存在過的“臟”數據。
    • 共享鎖的作用:讀線程A持有共享鎖,會阻止寫線程B獲取獨占鎖。因此,寫操作根本無法開始,讀操作讀到的絕對是寫操作開始前的一致狀態。
  • 問題二:讀到一個“正在變化”的值

    • 場景:數據可能不是一個簡單的 int,而是一個需要多條指令才能更新的復雜結構(例如,一個鏈表頭指針)。寫線程的更新操作可能不是原子的。
    • 此時:讀線程可能在寫線程更新到一半時介入,讀到錯誤的指針,導致程序崩潰或得到錯誤結果。
    • 共享鎖的作用:同樣,共享鎖阻止了寫線程的進入,保證了讀線程看到的對象狀態始終是完整的、穩定的。

2. 協調“讀”與“寫”的競爭關系

這是您問題描述中的關鍵點,也是 std::shared_mutex 設計的精髓。

  • 寫操作需要獨占性:寫操作必須保證在它修改數據的那一刻,沒有任何其他線程(無論是讀還是寫)正在訪問它要修改的數據。否則就無法安全地更新。
  • 共享鎖是給寫操作看的“信號”:讀線程加共享鎖,本質上是在向寫線程宣告:“我正在讀這份數據,你(寫線程)現在不能寫!
    • 只要有一個讀線程持有共享鎖,寫線程的獨占鎖請求就會被阻塞。
    • 多個讀線程的共享鎖不會互相阻塞,但它們“合力”阻塞了寫線程。
    • 寫線程持有獨占鎖時,會阻塞所有新的讀線程(共享鎖)和其他寫線程。

總結

所以,回到您的問題:讀數據對數據沒有影響,為什么還需要 shared_mutex

  1. 為了保護讀操作自身:確保它不會讀到寫操作產生的中間狀態或臟數據,從而保證讀取結果的正確性和一致性
  2. 為了協調讀與寫的競爭:共享鎖是一個高效的“信號機制”,它允許多個讀操作并行(提升性能),同時強制讀操作和寫操作之間互斥(保證安全)。它實現了您所說的“讀讀共享、讀寫互斥、寫寫互斥”的完美規則。

如果沒有共享鎖,在C++應用程序層面,單純的讀操作在多線程環境下就是“裸奔”,無法保證自己能讀到有意義的數據。std::shared_mutex 就是為讀操作穿上的一件防護服,它不阻止其他讀操作也穿上同樣的防護服(共享),但能有效阻止寫操作(獨占)進來搞破壞。

好的,我們來詳細介紹 C++17 中的 std::shared_mutex(共享互斥量,俗稱讀寫鎖)的使用。

核心概念

std::shared_mutex 的核心是區分兩種訪問模式,對應兩種鎖:

  1. 共享模式 (Shared Mode) - 用于“讀”

    • 多個線程可以同時獲得共享鎖。
    • 當一個或多個線程持有共享鎖時,任何請求獨占鎖的線程都會被阻塞。
    • 使用 std::shared_lock 來管理共享鎖。
  2. 獨占模式 (Exclusive Mode) - 用于“寫”

    • 只有一個線程可以獲得獨占鎖。
    • 當一線程持有獨占鎖時,任何其他請求共享鎖獨占鎖的線程都會被阻塞。
    • 使用 std::unique_lockstd::lock_guard 來管理獨占鎖。

包含頭文件

#include <shared_mutex> // 主要頭文件
#include <mutex>        // 用于 std::unique_lock, std::lock_guard
#include <map>
#include <string>
#include <thread>

基本使用步驟

1. 定義共享數據和共享互斥量

將你需要保護的數據和對應的 std::shared_mutex 放在一起,通常作為類的私有成員。

class ThreadSafeDNSCache {
private:std::map<std::string, std::string> dns_map_;mutable std::shared_mutex mutex_; // ‘mutable’ 允許在 const 成員函數中加共享鎖
};
2. 讀操作 - 使用 std::shared_lock

對于不會修改數據的操作(如 find, get),使用 std::shared_lock。它會在構造時自動上共享鎖,析構時自動解鎖。

std::string ThreadSafeDNSCache::find_ip(const std::string& domain) const {std::shared_lock<std::shared_mutex> lock(mutex_); // 獲取共享鎖// 注意:這里是 const 成員函數,因為find操作不應修改數據auto it = dns_map_.find(domain);if (it != dns_map_.end()) {return it->second; // 返回時,lock 析構,自動釋放共享鎖}return "Not Found";
}
3. 寫操作 - 使用 std::unique_lockstd::lock_guard

對于會修改數據的操作(如 insert, update, erase),使用 std::unique_lockstd::lock_guard。它們會在構造時自動上獨占鎖,析構時自動解鎖。

std::unique_lockstd::lock_guard 更靈活(例如可以手動解鎖),但開銷稍大。對于簡單作用域,std::lock_guard 就足夠了。

void ThreadSafeDNSCache::update_or_add(const std::string& domain, const std::string& ip) {std::unique_lock<std::shared_mutex> lock(mutex_); // 獲取獨占鎖dns_map_[domain] = ip;
} // lock 析構,自動釋放獨占鎖void ThreadSafeDNSCache::clear_all() {std::lock_guard<std::shared_mutex> lock(mutex_); // 同樣獲取獨占鎖dns_map_.clear();
}

完整示例代碼

#include <iostream>
#include <map>
#include <string>
#include <shared_mutex>
#include <thread>
#include <chrono>class ThreadSafeDNSCache {
public:std::string find_ip(const std::string& domain) const {// 1. 嘗試獲取共享鎖(讀鎖)std::shared_lock<std::shared_mutex> lock(mutex_);std::cout << "Reading domain: " << domain << std::endl;// 模擬一個耗時較長的讀操作std::this_thread::sleep_for(std::chrono::milliseconds(100));auto it = dns_map_.find(domain);if (it != dns_map_.end()) {std::cout << "Found IP: " << it->second << " for domain: " << domain << std::endl;return it->second;}std::cout << "Domain not found: " << domain << std::endl;return "Not Found";}void update_or_add(const std::string& domain, const std::string& ip) {// 2. 嘗試獲取獨占鎖(寫鎖)std::unique_lock<std::shared_mutex> lock(mutex_);std::cout << "Updating/Adding domain: " << domain << " -> " << ip << std::endl;// 模擬一個耗時較長的寫操作std::this_thread::sleep_for(std::chrono::milliseconds(500));dns_map_[domain] = ip;std::cout << "Finished updating: " << domain << std::endl;}private:mutable std::shared_mutex mutex_;std::map<std::string, std::string> dns_map_;
};int main() {ThreadSafeDNSCache cache;// 啟動多個讀線程和一個寫線程來演示效果std::thread reader1([&cache]() { cache.find_ip("github.com"); });std::thread reader2([&cache]() { cache.find_ip("google.com"); });std::thread writer([&cache]() {std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 稍等一下,讓讀線程先啟動cache.update_or_add("github.com", "140.82.112.4");});std::thread reader3([&cache]() { cache.find_ip("github.com"); }); // 這個讀操作會在寫之后開始reader1.join();reader2.join();writer.join();reader3.join();return 0;
}

可能輸出及分析:

輸出可能會是這樣的(順序可能略有不同):

Reading domain: github.com    // 讀者1 立即獲取共享鎖,開始讀
Reading domain: google.com    // 讀者2 也立即獲取共享鎖,和讀者1同時讀
// ... 讀者1和2 幾乎同時完成他們的讀操作 ...
Updating/Adding domain: github.com -> 140.82.112.4 // 寫者 等待讀者1和2釋放共享鎖后,獲取獨占鎖,開始寫
Finished updating: github.com // 寫者 完成寫操作,釋放獨占鎖
Reading domain: github.com    // 讀者3 在寫者釋放鎖后,獲取共享鎖,開始讀(會讀到新值)
Found IP: 140.82.112.4 for domain: github.com

這個輸出完美展示了:

  • 讀讀并行reader1reader2 同時執行。
  • 讀寫互斥writer 必須等待所有現有的讀者 (reader1, reader2) 結束后才能開始。
  • 寫寫互斥:(本例未展示第二個寫者)如果有第二個寫者,它也會被阻塞。
  • 寫后讀reader3writer 阻塞,直到寫操作完成,從而保證了它讀到的是最新值。

重要注意事項

  1. mutable 關鍵字:如果你的“讀”操作是 const 成員函數(它應該是),但你又需要在其中修改 mutex_(加鎖解鎖屬于“物理常量性”修改,而非“邏輯常量性”),必須用 mutable 修飾 mutex_
  2. 遞歸使用std::shared_mutex 是不可遞歸的。同一個線程試圖在已獲得共享鎖的情況下再獲取獨占鎖(或反之)會導致未定義行為(通常是死鎖)
  3. 升級鎖:不能直接將已持有的共享鎖“升級”為獨占鎖。你必須先釋放共享鎖,然后再嘗試獲取獨占鎖。這個過程不是原子的,中間可能被其他寫線程插隊。
  4. 性能:雖然讀寫鎖在“讀多寫少”的場景下性能優異,但其內部實現比普通互斥量更復雜,開銷也稍大。如果臨界區非常小,或者寫操作很頻繁,可能普通的 std::mutex 性能更好。永遠基于性能測試來做選擇

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/921419.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/921419.shtml
英文地址,請注明出處:http://en.pswp.cn/news/921419.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Nestjs框架: 基于裝飾器與Guards的完成RBAC權限系統設計與實現

概述 在現代權限管理系統中&#xff0c;RBAC&#xff08;基于角色的訪問控制&#xff09;是廣泛采用的一種模型RBAC 核心思想是通過角色來管理用戶權限通過角色綁定用戶、資源和權限&#xff0c;實現細粒度的訪問控制為了實現這一目標&#xff0c;我們需要在數據庫中設計合理的…

機器學習如何精準預測高值

一、概念理解“機器學習對于高值的預測保守”&#xff0c;這是建模里很常見的現象&#xff0c;尤其在生態、氣候、遙感這類數據分布高度偏斜的場景。通常可以從以下幾個角度理解&#xff1a;1. 數據分布與樣本稀缺在訓練集里&#xff0c;高值樣本往往非常少&#xff0c;遠低于中…

蜂窩物聯網模組:智能門禁產品上的關鍵部件

隨著物聯網技術的快速發展&#xff0c;蜂窩物聯網模組正逐步成為智能門禁系統的關鍵通信組件。蜂窩模組憑借其廣覆蓋、高可靠性和低功耗特性&#xff0c;正從傳統門禁系統的補充角色轉變為智能門禁的核心通信組件&#xff0c;尤其在智慧社區、商業樓宇和政府機構等場景中展現出…

[光學原理與應用-417]:非線性光學 - 線性光學(不引發頻率的變化)與非線性光學(引發頻率變化)的異同

一、定義與物理機制&#xff1a;線性響應 vs 非線性響應線性光學定義&#xff1a;光與物質相互作用時&#xff0c;介質的極化強度與入射光電場強度呈線性關系&#xff08;P?0?χ(1)E&#xff09;&#xff0c;輸出光強與輸入光強成正比&#xff08;Iout?∝Iin?&#xff09;-…

深入探討AI在三大核心測試場景中的應用

隨著人工智能&#xff08;AI&#xff09;技術的迅猛發展&#xff0c;軟件測試領域正經歷深刻變革。傳統手動測試和基于規則的自動化測試已難以應對日益復雜的系統架構與海量用戶行為。AI測試通過引入機器學習、自然語言處理、計算機視覺等技術&#xff0c;顯著提升了測試效率、…

[linux倉庫]性能加速的隱形引擎:深度解析Linux文件IO中的緩沖區奧秘

&#x1f31f; 各位看官好&#xff0c;我是egoist2023&#xff01; &#x1f30d; Linux Linux is not Unix &#xff01; &#x1f680; 今天來學習C語言緩沖區和內核緩存區的區別以及緩存類型。 &#x1f44d; 如果覺得這篇文章有幫助&#xff0c;歡迎您一鍵三連&#xff0c…

一、計算機的數據存儲

計算機的世界只有0和1。 1.1 進制 十進制整數->二進制整數&#xff1a;除2倒取余二進制->十進制&#xff1a;權值相加法 結論&#xff1a;1位8進制值 3位二進制值&#xff0c;1位十六進制值 4位二進制值 public class JinZhiDemo {public static void main(String[]…

SpringBoot集成XXL-JOB保姆教程

第一步&#xff1a; 下載xxl-job源碼到本地&#xff0c;地址如下&#xff1a; xxl-job: 一個分布式任務調度平臺&#xff0c;其核心設計目標是開發迅速、學習簡單、輕量級、易擴展。現已開放源代碼并接入多家公司線上產品線&#xff0c;開箱即用。 第二步&#xff1a; 創建…

Debezium日常分享系列之:Debezium 3.2.2.Final發布

Debezium日常分享系列之&#xff1a;Debezium 3.2.2.Final發布Debezium CoreConnector啟動時出現難以理解的錯誤臨時阻塞快照失敗可能導致數據丟失的問題修復Debezium for OracleDebezium CoreConnector 啟動時出現難以理解的錯誤 我們解決了一個問題&#xff0c;即連接器會因…

Zoom AI 技術架構研究:聯合式方法與多模態集成

一、研究背景與概述 在當今數字化轉型加速的背景下,人工智能技術正深刻改變企業協作與溝通方式。作為全球領先的視頻會議平臺,Zoom 已從單純的通信工具轉型為全面的生產力平臺,而其 AI 技術架構是這一轉變的核心驅動力。本報告將深入分析 Zoom 的 AI 技術架構,特別是其創新…

排序-快速排序 O(n log n)

快排&#xff1a;1、設定一個中間值 q[ lr >>1 ] , 讓左右區間來比較2、左邊通過 i 依次比較&#xff0c;如果比這個中間值小&#xff0c;就繼續 , 直到不符合3、右邊通過 j-- 依次比較&#xff0c;如果比這個中間值大&#xff0c;就繼續 &#xff0c;直到不符合4、兩邊…

【Proteus仿真】定時器控制系列仿真——LED小燈閃爍/流水燈/LED燈帶控制/LED小燈實現二進制

目錄 0案例視頻效果展示 0.1例子1&#xff1a;基于AT89C51單片機的定時器控制小燈閃爍 0.2例子2&#xff1a;基于AT89C51單片機的定時器T0流水燈 0.3例子3&#xff1a;基于AT89C51單片機的定時器控制LED燈帶 0.4例子4&#xff1a;基于AT89C51單片機的定時器控制LED閃爍 0…

進階向:密碼生成與管理工具

密碼生成與管理工具&#xff1a;從零開始的完全指南在現代數字生活中&#xff0c;密碼是保護個人信息和賬戶安全的第一道防線。隨著網絡服務的普及&#xff0c;每個人平均需要管理數十個不同賬戶的密碼。一個強大且獨特的密碼通常應包含12個以上字符&#xff0c;混合大小寫字母…

解決 Gitee 中 git push 因郵箱隱私設置導致的失敗問題

解決 Gitee 中 git push 因郵箱隱私設置導致的失敗問題 在使用 Git 向 Gitee 遠程倉庫推送代碼時&#xff0c;可能會遇到因郵箱隱私設置引發的 git push 失敗情況。最近我就碰到了&#xff0c;現在把問題現象、原因和解決方法分享出來。 一、錯誤現象 執行 git push -u origin …

Flutter的三棵樹

“三棵樹”是 Flutter 渲染和構建UI的核心機制&#xff0c;理解它們對于掌握 Flutter 至關重要。這三棵樹分別是&#xff1a; Widget 樹 Element 樹 RenderObject 樹 它們協同工作&#xff0c;以實現 Flutter 的高性能渲染和高效的響應式編程模型。 Flutter 是聲明式的UI&…

同一臺nginx中配置多個前端項目的三種方式

目錄 第一種方式:配置多個二級域名 第二種方式:配置端口轉發(不推薦) 第三種方式:同一個server中基于location配置(重點講解) 第一種方式:配置多個二級域名 一個域名下面申請多個二級域名,每個二級域名配置一個vue前端項目,這個很好配置,在這里不再詳細說明。 …

第二家公司雖然用PowerBI ,可能更適合用以前的QuickBI

第二家公司雖然用PowerBI &#xff0c;可能更適合用以前的QuickBI現在回想一下&#xff0c;第二家公司數據源是MySQL &#xff0c;常規報表是用excel報表&#xff0c;另外還做了一張能發布到web的看板供運營使用。基于基本情況&#xff0c;quickbi 的早期版本是合適的&#xff…

STM32 USBx Device HID standalone 移植示例 LAT1466

關鍵字&#xff1a;USBx&#xff0c; Device, HID&#xff0c;standalone 1.設計目的 目前 USBx Device standalone 的官方示例較少&#xff0c;不過使用 STM32CubeMX 可以快速地生成 USBx Device 相關類的示例工程&#xff0c;會很方便大家的開發。這里以 NUCLEO-H563 為例&…

python創建并寫入excel文件

大家好&#xff0c;這里是七七&#xff0c;今天來跟大家分享一個python創建并寫入一個excel文件的小例子&#xff0c;話不多說&#xff0c;開始介紹。首先我們來看一下這一小段代碼。import openpyxl# 創建一個新的 Excel 工作簿workbook openpyxl.Workbook()# 獲取當前活動的…

react native 出現 FATAL EXCEPTION: OkHttp Dispatcher

react native 出現 FATAL EXCEPTION: OkHttp Dispatcher 報錯信息FATAL EXCEPTION: OkHttp DispatcherProcess: , PID: 8868java.lang.NoSuchMethodError: No virtual method toString(Z)Ljava/lang/String; in class Lokhttp3/Cookie; or its super classes (declaration of o…