C++11線程同步之互斥鎖

C++11線程同步之互斥鎖

  • std::mutex
    • 成員函數
    • 線程同步
  • std::lock_guard
  • std::recursive_mutex
  • std::timed_mutex

進行多線程編程,如果多個線程需要對同一塊內存進行操作,比如:同時讀、同時寫、同時讀寫對于后兩種情況來說,如果不做任何的人為干涉就會出現各種各樣的錯誤數據。這是因為線程在運行的時候需要先得到CPU時間片,時間片用完之后需要放棄已獲得的CPU資源,就這樣線程頻繁地在就緒態和運行態之間切換,更復雜一點還可以在就緒態、運行態、掛起態之間切換,這樣就會導致線程的執行順序并不是有序的,而是隨機的混亂的,就如同下圖中的這個例子一樣,理想很豐滿現實卻很殘酷。
在這里插入圖片描述
解決多線程數據混亂的方案就是進行線程同步,最常用的就是互斥鎖,在C++11中一共提供了四種互斥鎖:

  • std::mutex:獨占的互斥鎖,不能遞歸使用
  • std::timed_mutex:帶超時的獨占互斥鎖,不能遞歸使用
  • std::recursive_mutex:遞歸互斥鎖,不帶超時功能
  • std::recursive_timed_mutex:帶超時的遞歸互斥鎖

std::mutex

不論是在C還是C++中,進行線程同步的處理流程基本上是一致的,C++的mutex類提供了相關的API函數:

成員函數

lock()函數用于給臨界區加鎖,并且只能有一個線程獲得鎖的所有權,它有阻塞線程的作用。
函數原型如下:

void lock();

獨占互斥鎖對象有兩種狀態:鎖定和未鎖定。如果互斥鎖是打開的,調用lock()函數的線程會得到互斥鎖的所有權,并將其上鎖,其它線程再調用該函數的時候由于得不到互斥鎖的所有權,就會被lock()函數阻塞。當擁有互斥鎖所有權的線程將互斥鎖解鎖,此時被lock()阻塞的線程解除阻塞,搶到互斥鎖所有權的線程加鎖并繼續運行,沒搶到互斥鎖所有權的線程繼續阻塞。

除了使用lock()還可以使用try_lock()獲取互斥鎖的所有權并對互斥鎖加鎖,函數原型如下:

bool try_lock();

二者的區別在于try_lock()不會阻塞線程,lock()會阻塞線程:

  • 如果互斥鎖是未鎖定狀態,得到了互斥鎖所有權并加鎖成功,函數返回true
  • 如果互斥鎖是鎖定狀態,無法得到互斥鎖所有權加鎖失敗,函數返回false

當互斥鎖被鎖定之后可以通過unlock()進行解鎖,但是需要注意的是只有擁有互斥鎖所有權的線程也就是對互斥鎖上鎖的線程才能將其解鎖,其它線程是沒有權限做這件事情的。該函數的函數原型如下:

void unlock();

通過介紹以上三個函數,使用互斥鎖進行線程同步的大致思路差不多就能搞清楚了,主要分為以下幾步:

  • 找到多個線程操作的共享資源(全局變量、堆內存、類成員變量等),也可以稱之為臨界資源
  • 找到和共享資源有關的上下文代碼,也就是臨界區(下圖中的黃色代碼部分)
  • 在臨界區的上邊調用互斥鎖類的lock()方法
  • 在臨界區的下邊調用互斥鎖的unlock()方法

線程同步的目的是讓多線程按照順序依次執行臨界區代碼,這樣做線程對共享資源的訪問就從并行訪問變為了線性訪問,訪問效率降低了,但是保證了數據的正確性。
在這里插入圖片描述
注意:
當線程對互斥鎖對象加鎖,并且執行完臨界區代碼之后,一定要使用這個線程對互斥鎖解鎖,否則最終會造成線程的死鎖。死鎖之后當前應用程序中的所有線程都會被阻塞,并且阻塞無法解除,應用程序也無法繼續運行。

線程同步

舉個栗子,我們讓兩個線程共同操作同一個全局變量,二者交替數數,將數值存儲到這個全局變量里邊并打印出來。

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
using namespace std;int g_num = 0;  // 為 g_num_mutex 所保護
mutex g_num_mutex;void slow_increment(int id)
{for (int i = 0; i < 3; ++i) {g_num_mutex.lock();++g_num;cout << id << " => " << g_num << endl;g_num_mutex.unlock();this_thread::sleep_for(chrono::seconds(1));}
}int main()
{thread t1(slow_increment, 0);thread t2(slow_increment, 1);t1.join();t2.join();
}

在上面的示例程序中,兩個子線程執行的任務的一樣的(其實也可以不一樣,不同的任務中也可以對共享資源進行讀寫操作),在任務函數中把與全局變量相關的代碼加了鎖,兩個線程只能順序訪問這部分代碼(如果不進行線程同步打印出的數據是混亂且無序的)。另外需要強調一點:

  • 在所有線程的任務函數執行完畢之前,互斥鎖對象是不能被析構的,一定要在程序中保證這個對象的可用性。
  • 互斥鎖的個數和共享資源的個數相等,也就是說每一個共享資源都應該對應一個互斥鎖對象。互斥鎖對象的個數和線程的個數沒有關系。

std::lock_guard

lock_guard是C++11新增的一個模板類,使用這個類,可以簡化互斥鎖lock()和unlock()的寫法,同時也更安全。這個模板類的定義和常用的構造函數原型如下:

// 類的定義,定義于頭文件 <mutex>
template< class Mutex >
class lock_guard;// 常用構造函數
explicit lock_guard( mutex_type& m );

lock_guard在使用上面提供的這個構造函數構造對象時,會自動鎖定互斥量,而在退出作用域后進行析構時就會自動解鎖,從而保證了互斥量的正確操作,避免忘記unlock()操作而導致線程死鎖。lock_guard使用了RAII技術,就是在類構造函數中分配資源,在析構函數中釋放資源,保證資源出了作用域就釋放。

使用lock_guard對上面的例子進行修改,代碼如下:

void slow_increment(int id)
{for (int i = 0; i < 3; ++i) {// 使用哨兵鎖管理互斥鎖lock_guard<mutex> lock(g_num_mutex);++g_num;cout << id << " => " << g_num << endl;this_thread::sleep_for(chrono::seconds(1));}
}

通過修改發現代碼被精簡了,而且不用擔心因為忘記解鎖而造成程序的死鎖,但是這種方式也有弊端,在上面的示例程序中整個for循環的體都被當做了臨界區,多個線程是線性的執行臨界區代碼的,因此臨界區越大程序效率越低,還是需要根據實際情況選擇最優的解決方案。

std::recursive_mutex

遞歸互斥鎖std::recursive_mutex允許同一線程多次獲得互斥鎖,可以用來解決同一線程需要多次獲取互斥量時死鎖的問題,在下面的例子中使用獨占非遞歸互斥量會發生死鎖:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;struct Calculate
{Calculate() : m_i(6) {}void mul(int x){lock_guard<mutex> locker(m_mutex);m_i *= x;}void div(int x){lock_guard<mutex> locker(m_mutex);m_i /= x;}void both(int x, int y){lock_guard<mutex> locker(m_mutex);mul(x);div(y);}int m_i;mutex m_mutex;
};int main()
{Calculate cal;cal.both(6, 3);return 0;
}

上面的程序中執行了cal.both(6, 3);調用之后,程序就會發生死鎖,在both()中已經對互斥鎖加鎖了,繼續調用mult()函數,已經得到互斥鎖所有權的線程再次獲取這個互斥鎖的所有權就會造成死鎖(在C++中程序會異常退出,使用C庫函數會導致這個互斥鎖永遠無法被解鎖,最終阻塞所有的線程)。要解決這個死鎖的問題,一個簡單的辦法就是使用遞歸互斥鎖std::recursive_mutex,它允許一個線程多次獲得互斥鎖的所有權。修改之后的代碼如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;struct Calculate
{Calculate() : m_i(6) {}void mul(int x){lock_guard<recursive_mutex> locker(m_mutex);m_i *= x;}void div(int x){lock_guard<recursive_mutex> locker(m_mutex);m_i /= x;}void both(int x, int y){lock_guard<recursive_mutex> locker(m_mutex);mul(x);div(y);}int m_i;recursive_mutex m_mutex;
};int main()
{Calculate cal;cal.both(6, 3);cout << "cal.m_i = " << cal.m_i << endl;return 0;
}

雖然遞歸互斥鎖可以解決同一個互斥鎖頻繁獲取互斥鎖資源的問題,但是還是建議少用,主要原因如下:

  • 使用遞歸互斥鎖的場景往往都是可以簡化的,使用遞歸互斥鎖很容易放縱復雜邏輯的產生,從而導致bug的產生
  • 遞歸互斥鎖比非遞歸互斥鎖效率要低一些。
  • 遞歸互斥鎖雖然允許同一個線程多次獲得同一個互斥鎖的所有權,但最大次數并未具體說明,一旦超過一定的次數,就會拋出std::system錯誤。

std::timed_mutex

std::timed_mutex是超時獨占互斥鎖,主要是在獲取互斥鎖資源時增加了超時等待功能,因為不知道獲取鎖資源需要等待多長時間,為了保證不一直等待下去,設置了一個超時時長,超時后線程就可以解除阻塞去做其他事情了。

std::timed_mutex比std::_mutex多了兩個成員函數:try_lock_for()和try_lock_until():

void lock();
bool try_lock();
void unlock();// std::timed_mutex比std::_mutex多出的兩個成員函數
template <class Rep, class Period>bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);template <class Clock, class Duration>bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
  • try_lock_for函數是當線程獲取不到互斥鎖資源的時候,讓線程阻塞一定的時間長度
  • try_lock_until函數是當線程獲取不到互斥鎖資源的時候,讓線程阻塞到某一個指定的時間點
  • 關于兩個函數的返回值:當得到互斥鎖的所有權之后,函數會馬上解除阻塞,返回true,如果阻塞的時長用完或者到達指定的時間點之后,函數也會解除阻塞,返回false

下面的示例程序中為大家演示了std::timed_mutex的使用:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;timed_mutex g_mutex;void work()
{chrono::seconds timeout(1);while (true){// 通過阻塞一定的時長來爭取得到互斥鎖所有權if (g_mutex.try_lock_for(timeout)){cout << "當前線程ID: " << this_thread::get_id() << ", 得到互斥鎖所有權..." << endl;// 模擬處理任務用了一定的時長this_thread::sleep_for(chrono::seconds(10));// 互斥鎖解鎖g_mutex.unlock();break;}else{cout << "當前線程ID: " << this_thread::get_id() << ", 沒有得到互斥鎖所有權..." << endl;// 模擬處理其他任務用了一定的時長this_thread::sleep_for(chrono::milliseconds(50));}}
}int main()
{thread t1(work);thread t2(work);t1.join();t2.join();return 0;
}

示例代碼輸出的結果:

當前線程ID: 125776, 得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 沒有得到互斥鎖所有權...
當前線程ID: 112324, 得到互斥鎖所有權...

在上面的例子中,通過一個while循環不停的去獲取超時互斥鎖的所有權,如果得不到就阻塞1秒鐘,1秒之后如果還是得不到阻塞50毫秒,然后再次繼續嘗試,直到獲得互斥鎖的所有權,跳出循環體。

關于遞歸超時互斥鎖std::recursive_timed_mutex的使用方式和std::timed_mutex是一樣的,只不過它可以允許一個線程多次獲得互斥鎖所有權,而std::timed_mutex只允許線程獲取一次互斥鎖所有權。另外,遞歸超時互斥鎖std::recursive_timed_mutex也擁有和std::recursive_mutex一樣的弊端,不建議頻繁使用。

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

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

相關文章

《互聯網的世界》第四講-擁塞控制與編碼

需要澄清的一個誤區是&#xff0c;擁塞絕不是發送的數據量太大導致&#xff0c;而是數據在極短的時間段內到達了同一個地方以至于超過了網絡處理容量導致&#xff0c;擁塞的成因一定要考慮時間因素。換句話說&#xff0c;擁塞由大突發導致。 只要 pacing&#xff0c;再多的數據…

2024.3.4訓練記錄(8)

文章目錄 CF 459D Pashmak and Parmidas problemCF 1388C Uncle Bogdan and Country HappinessCF 1525D ArmchairsCF 220B Little Elephant and Array CF 459D Pashmak and Parmida’s problem 題目鏈接 最近感覺對數據結構題的反應度提升了&#xff0c;這一題是上午看的但是…

動態規劃(算法競賽、藍橋杯)--樹形DP樹形背包

1、B站視頻鏈接&#xff1a;E18 樹形DP 樹形背包_嗶哩嗶哩_bilibili #include <bits/stdc.h> using namespace std; const int N110; int n,V,p,root; int v[N],w[N]; int h[N],to[N],ne[N],tot; //鄰接表 int f[N][N];void add(int a,int b){to[tot]b;ne[tot]h[a];h[a…

數倉項目6.0(一)

尚硅谷大數據項目【電商數倉6.0】企業數據倉庫項目_bilibili 數據流轉過程 用戶??業務服務器??數據庫存儲??數倉統計分析??數據可視化 數據倉庫處理流程&#xff1a;數據源??加工數據??統計篩選數據??分析數據 數據庫不是為了數據倉庫服務的&#xff0c;需要…

B084-SpringCloud-Zuul Config

目錄 zuul系統架構和zuul的作用zuul網關實現配置映射路徑過濾器 Config概述云端管理本地配置 zuul zuul是分布式和集群后前端統一訪問入口 系統架構和zuul的作用 zuul把自己注冊進eureka&#xff0c;然后可通過前端傳來的服務名發現和訪問對應的服務集群 為了預防zuul單點故…

Java 枚舉類的深入理解與應用

Java 的枚舉類是一種特殊的類&#xff0c;通常表示一組常量。在編譯或設計時&#xff0c;當我們知道所有變量的可能性時&#xff0c;盡量使用枚舉類型。本文將通過一個具體的例子&#xff0c;深入探討 Java 枚舉類的定義、使用和高級特性。 目錄 枚舉類的定義與使用枚舉類的構造…

【OJ】求和與計算日期

文章目錄 1. 前言2. JZ64 求123...n2.1 題目分析2.2 代碼 3. HJ73 計算日期到天數轉換3.1 題目分析3.2 代碼 4. KY222 打印日期4.1 題目分析4.2 代碼 1. 前言 下面兩個題目均來自牛客&#xff0c;使用的編程語言是c&#xff0c;分享個人的一些思路和代碼。 2. JZ64 求123…n …

Vue 賦值后原數據隨賦值后的數據的變化而變化

很常見的&#xff0c;當我們直接用“”號等方式直接賦值后 原數據會隨賦值后的數據的變化而變化 但是有時候我們的需求是不需要原數據跟隨變化 所以怎么解決呢&#xff1f; 解決辦法有&#xff1a; 1.使用Object.assign() 方法 2.使用深拷貝函數 JSON.parse() 3.使用第三方庫lo…

畢業生信息招聘平臺|基于springboot+ Mysql+Java的畢業生信息招聘平臺設計與實現(源碼+數據庫+文檔+PPT)

目錄 論文參考 摘 要 數據庫設計 系統詳細設計 文末獲取源碼聯系 論文參考 摘 要 隨著社會的發展&#xff0c;社會的各行各業都在利用信息化時代的優勢。計算機的優勢和普及使得各種信息系統的開發成為必需。 畢業生信息招聘平臺&#xff0c;主要的模塊包括查看管理員&a…

#ifndef 和 #pragma once的區別

#ifndef 和 #pragma once 都是用來防止頭文件被重復包含的&#xff0c;但它們的工作方式和兼容性有所不同&#xff1a; #ifndef 是 C 的標準語法&#xff0c;它依賴于不重復的宏名稱&#xff0c;保證了包含在 #endif 的內容不會被重復包含。這個內容可以是一個文件的所有內容&…

Webpack配置與運行基礎教程

在前端開發中&#xff0c;Webpack是一款非常流行的模塊打包工具&#xff0c;它可以幫助我們將多個文件打包成一個或多個靜態資源文件&#xff0c;從而提高前端項目的性能和可維護性。本文將為你介紹Webpack的基礎配置和運行方法&#xff0c;幫助你快速上手Webpack。 什么是Web…

基于Springboot的無人智慧超市管理系統(有報告)。Javaee項目,springboot項目。

演示視頻&#xff1a; 基于Springboot的無人智慧超市管理系統&#xff08;有報告&#xff09;。Javaee項目&#xff0c;springboot項目。 項目介紹&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三層體系…

1.3 有哪些文本表示模型?它們各有什么優缺點?

1.3 有哪些文本表示模型?它們各有什么優缺點? 場景描述 文本是一類非常重要的非結構化數據&#xff0c;如何表示文本數據一直是機器學習領域的一個重要研究方向。 知識點 詞袋模型(Bag of Words)TF-IDF(Term Frequency-Inverse DocumentFrequency)主題模型(Topic Model)詞…

【每日刷題】數組-LC56、LC238、隨想錄1、LC560

1. LC56 合并區間 題目鏈接 Arrays.sort先讓intervals里的子數組按照子數組的第一個數字值從小到大排列。開一個新數組&#xff0c;newInterval&#xff0c;存放合并好的子數組讓intervals的當前子數組i的第一個數字與newInterval的當前子數組index的最后一個數字比較大小&am…

ARM 架構下國密算法庫

目錄 前言GmSSL編譯環境準備下載 GmSSL 源碼編譯 GmSSL 源碼SM4 對稱加密算法SM2 非對稱加密算法小結前言 在當前的國際形式下,國替勢不可擋。操作系統上,銀河麒麟、統信 UOS、鴻蒙 OS 等國產系統開始發力,而 CPU 市場,也是百花齊放,有 龍芯(LoongArch架構)、兆芯(X86…

Intel/國產化無人叉車機器視覺專用控制器

無人叉車和機器視覺是兩個獨立的技術領域&#xff0c;但它們可以結合使用以實現更高效的物流自動化。無人叉車是一種自動化運輸工具&#xff0c;可以在沒有人為干預的情況下完成貨物的搬運和運輸。機器視覺是一種人工智能技術&#xff0c;可以讓計算機識別和理解圖像或視頻中的…

YOLO:實時目標檢測的革命

目標檢測作為計算機視覺領域的一個核心任務&#xff0c;一直以來都是研究的熱點。而YOLO&#xff08;You Only Look Once&#xff09;技術作為其中的杰出代表&#xff0c;以其獨特的處理方式和卓越的性能&#xff0c;成為了實時目標檢測的標桿。本文將探討YOLO技術的核心原理、…

FPGA時序約束與分析--建立時間與保持時間

文章目錄 前言一、定義二、舉例說明2.1 建立時間違規2.2 保持時間違規前言 時序約束的定義–設計者根據實際的系統功能,通過時序約束的方式提出時序要求; FPGA 編譯工具根據設計者的時序要求,進行布局布線;編譯完成后, FPGA 編譯工具還需要針對布局布線的結果,套用特定的…

【C++】每日一題,189 輪轉數組

給定一個整數數組 nums&#xff0c;將數組中的元素向右輪轉 k 個位置&#xff0c;其中 k 是非負數。 示例 1: 輸入: nums [1,2,3,4,5,6,7], k 3 輸出: [5,6,7,1,2,3,4] 解釋: 向右輪轉 1 步: [7,1,2,3,4,5,6] 向右輪轉 2 步: [6,7,1,2,3,4,5] 向右輪轉 3 步: [5,6,7,1,2,3,…

搜索回溯算法(DFS)1------遞歸

目錄 簡介&#xff1a; 遞歸問題解題的思路模板 例題1&#xff1a;漢諾塔 例題2&#xff1a;合并兩個有序鏈表 例題3&#xff1a;反轉鏈表 例題4&#xff1a;兩兩交換鏈表中的節點 例題5&#xff1a;Pow&#xff08;x,n&#xff09;-快速冪 結語&#xff1a; 簡介&…