【C++】多线程与互斥锁【二】
文章目錄
- 1. 并發是什么
- 1.1 并發與并行
- 1.2 硬件并發與任務切換
- 1.3 多線程并發與多進程并發
- 2. 為什么使用并發
- 2.1 為什么使用并發
- 2.2 并發和多線程
- 3. 并發需要注意的問題
- 3.1 多線程中的數據競爭
- 實例1:
- 3.2 如何處理數據競爭?
- 實例2:
- 實例3:
- 注意
- 3.3 C++11新標準多線程支持庫
- 3.4 **lock_guard與unique_lock保護共享資源*`**
- 3.4.1 lock_guard:
- 實例4:
- 實例5:
- 3.4.2 unique_lock
- 3.4.3 lock_guard與unique_lock的區別如下:
- 實例6:
- 3.5 [`timed_mutex`](`https://www.cplusplus.com/reference/mutex/timed_mutex/`)與[`recursive_mutex`](https://www.cplusplus.com/reference/mutex/recursive_mutex/)提供更強大的鎖
- 實例7:
- 4. 小結
1. 并發是什么
1.1 并發與并行
并發指的是兩個或多個獨立的活動在同一時段內發生。并發在生活中隨處可見:比如在跑步的時候同時聽音樂,在看電腦顯示器的同時敲擊鍵盤等。
與并發相近的另一個概念是并行。它們兩者存在很大的差別,圖示如下:
并發:同一時間段內可以交替處理多個操作,強調同一時段內交替發生。
并行:同一時刻內同時處理多個操作,強調同一時刻點同時發生。
1.2 硬件并發與任務切換
既然并發是在同一時間段內交替發生即可,不要求同時發生。單核心處理器上的多任務并發是靠任務切換實現的,跟多核處理器上的并行多任務處理還是有較大區別的,但對處理器的使用和多任務調度工作主要由操作系統完成了,所以我們在兩者之間編寫應用程序區別倒是不大。下面再貼個直觀的圖示:
- 雙核處理器并行執行(硬件并發)對比單核處理器并發執行(任務上下文切換)
-
雙核處理器均并發執行(一般任務數遠大于處理器核心數,多核并發更常見)
1.3 多線程并發與多進程并發
多任務并發,線程與進程,三者的主要區別如下:
-
任務:從我們認知角度抽象出來的一個概念,放到計算機上主要指由軟件完成的一個活動。一個任務既可以是一個進程,也可以是一個線程。簡而言之,它指的是一系列共同達到某一目的的操作。例如,讀取數據并將數據放入內存中。這個任務可以作為一個進程來實現,也可以作為一個線程(或作為一個中斷任務)來實現。
-
進程:資源分配的基本單位,也可能作為調度運行的單位。可以把一個進程看成是一個獨立的程序,在內存中有其完備的數據空間和代碼空間。一個進程所擁有的數據和變量只屬于它自己。例如,用戶運行自己的程序,系統就創建一個進程,并為它分配資源,包括各種表格、內存空間、磁盤空間、I/O設備等。然后,把該進程放人進程的就緒隊列。進程調度程序選中它,為它分配CPU以及其它有關資源,該進程才真正運行。所以,進程是系統中的并發執行的單位。
詳細見【操作系統(二)】
-
線程:執行處理器調度的基本單位。一個進程由一個或多個線程構成,各線程共享相同的代碼和全局數據,但各有其自己的堆棧。由于堆棧是每個線程一個,所以局部變量對每一線程來說是私有的。由于所有線程共享同樣的代碼和全局數據,它們比進程更緊密,比單獨的進程間更趨向于相互作用,線程間的相互作用更容易些,因為它們本身就有某些供通信用的共享內存:進程的全局數據。
詳細見【操作系統(三)】
- 多線程并發:在同一時間段內交替處理多個操作,線程切換時間片是很短的(毫秒級),一個時間片多數時候來不及處理完對某一資源的訪問;
- 線程間通信:一個任務被分割為多個線程并發處理,多個線程可能都要處理某一共享內存的數據,多個線程對同一共享內存數據的訪問需要準確有序。
如果像前一篇文章中的示例,雖然創建了三個線程,但線程間不需要訪問共同的內存分區實例,對線程間的執行順序沒有更多要求。但如果多個進程都需要訪問相同的共享內存數據,如果都是讀取數據還好,如果有讀取有寫入或者都要寫入(數據并發訪問或數據競爭),就需要使讀寫有序(同步化),否則可能會造成數據混亂,得不到我們預期的結果。下面再介紹兩個用于理解線程同步的概念:
- 同步:是指在不同進程之間的若干程序片斷,它們的運行必須嚴格按照規定的某種先后次序來運行,這種先后次序依賴于要完成的特定的任務。如果用對資源的訪問來定義的話,同步是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。
- 互斥:是指散布在不同進程之間的若干程序片斷,當某個進程運行其中一個程序片段時,其它進程就不能運行它們之中的任一程序片段,只能等到該進程運行完這個程序片段后才可以運行。如果用對資源的訪問來定義的話,互斥某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
多個線程對共享內存數據訪問的競爭條件的形成,取決于一個以上線程的相對執行順序,每個線程都搶著完成自己的任務。C++標準中對數據競爭的定義是:多個線程并發的去修改一個獨立對象,數據競爭是未定義行為的起因。
2. 為什么使用并發
2.1 為什么使用并發
使用并發的原因主要有兩個:關注點分離和性能**。
- 關注點分離:通過將相關的代碼放在一起并將無關的代碼分開,可以使你的程序更容易理解和測試,從而減少出錯的可能性。你可以使用并發來分隔不同的功能區域,即使在這些不同功能區域的操作需要在同一時刻發生的情況下;若不顯式地使用并發,你要么被迫編寫任務切換框架,要么在操作中主動地調用不相關的一段代碼。
- 更高效的性能:為了充分發揮多核心處理器的優勢,使用并發將單個任務分成幾部分且各自并行運行,從而降低總運行時間。根據任務分割方式的不同,又可以將其分為兩大類:一類是對同樣的數據應用不同的處理算法(任務并行);另一類是用同樣的處理算法共同處理數據的幾部分(數據并行)。本質上,目標就是加速,提高處理性能。
知道何時不使用并發與知道何時使用它一樣重要。基本上,不使用并發的唯一原因就是在收益比不上成本的時候。使用并發的代碼在很多情況下難以理解,額外的復雜性也可能導致更多的錯誤。除非潛在的性能增益足夠大或關注點分離地足夠清晰,能抵消確保其正確所需的額外的開發時間以及與維護多線程代碼相關的額外成本,否則不要使用并發。 一般multi task任務數遠大于線程數量時候,比如一個線程開啟4-8個乃至更多的task時候,操作系統可以更好的調度task的完成,程序粒度更加容易控制,多任務操作開銷遠小于或者小于性能提升時候,才開始使用,具體的task數量需要根據具體任務測試性能。
2.2 并發和多線程
早期的C++標準中,如1998 C++標準版不承認線程的存在,并且各種語言要素的操作效果都以順序抽象機的形式編寫。內存模型也沒有被正式定義,所以對于1998 C++標準,沒辦法在缺少編譯器相關擴展的情況下編寫多線程應用程序。如果在之前想使用多線程并發編程,可以借助編譯器廠商提供的平臺相關的擴展多線程支持API(比如POSIX C和Microsoft Windows API),但這種多線程支持對平臺依賴度較高,導致可移植性較差 。
為了解決平臺相關多線程API使用上的問題,逐漸開發出了Boost、ACE等平臺無關的多線程支持類庫。直到C++11標準的發布,借鑒了很多Boost類庫的經驗,將多線程支持納入C++標準庫。C++11標準不僅提供了一個全新的線程感知內存模型,也包含了用于管理線程、保護共享數據、線程間同步操作以及低級原子操作的各個類。
對于C++整體以及包含低級工具的C++類——特別是在新版C++線程庫里的那些,參與高性能計算的開發者常常關注的一點就是效率。如果你正尋求極致的性能,那么理解與直接使用底層的低級工具相比,使用高級工具所帶來的實現成本,是很重要的。這個成本就是抽象懲罰(abstraction penalty)。標準C++線程庫在設計時,就非常注重高效的性能,提供了足夠的低級工具(比如原子操作庫),以付出盡可能低的抽象懲罰。C++標準庫也提供了更高級別的抽象和工具,它們使得編寫多線程代碼更簡單和不易出錯。有時候運用這些工具確實會帶來性能成本,因為必須執行額外的代碼。但是這種性能成本并不一定意味著更高的抽象懲罰;總體來看,這種性能成本并不比通過手工編寫等效的函數而招致的成本更高,同時編譯器可能會很好地內聯大部分額外的代碼。
3. 并發需要注意的問題
3.1 多線程中的數據競爭
一個多線程C++程序是什么樣子的?它看上去和其他所有C++程序一樣,通常是變量、類以及函數的組合。唯一真正的區別在于某些函數可以并發運行,所以你需要確保共享數據的并發訪問是安全的。當然,為了并發地運行函數,必須使用特定的函數以及對象來管理各個線程。
多線程編程在許多領域是不可或缺的。但是,多線程并行,非常容易引發數據競爭,而且還非常不容易被發現和debug。下面,我們用C++語言來演示一下,什么是數據競爭:
實例1:
#include <iostream>
#include <stdlib.h>
#include <thread>
#include <string>#define COUNT 1000
volatile int num = 0;void thread1()
{for (int i=0; i<COUNT; i++){num++;}
}void thread2()
{for (int i=0; i<COUNT; i++){num--;}
}int main(int argc, char* argv[])
{std::thread t1(thread1);std::thread t2(thread2);t1.join();t2.join();std::cout << "final value:" << num << std::endl;getchar();return 0;
}
如果說沒有數據競爭(data race)的話,這兩個線程執行完畢后,數據最后一定是回到初始值0。然而,我們嘗試運行后發現,事與愿違,每次執行的結果都不是0,而且每次的結果都不一樣。
下面這種是正確的寫法:
std::thread t1(thread1);t1.join();std::thread t2(thread2);t2.join();
為什么會發生這樣的現象呢?因為為普通變量加1減1這樣的操作并非“原子”操作。我們簡化一下這個過程,它可以分為三個步驟,讀數據,執行計算,寫數據。理想情況下,我們期望的執行流程應該是這樣的:
然而,線程的調度是不受我們控制的,即便線程1和線程2內部的執行流程不變,只要調度時機發生了變化,結果也會不同,比如說,實際的執行過程中,有可能是這樣的情況:
隨著調度情況的不同,最終的結果也會有所差異,所以我們可以看到,這個程序的執行結果不是0,而且循環次數越多,發生數據競爭的機會也越大。
3.2 如何處理數據競爭?
從數據競爭形成的條件入手,數據競爭源于并發修改同一數據結構,那么最簡單的處理數據競爭的方法就是對該數據結構采用某種保護機制,確保只有進行修改的線程才能看到數據被修改的中間狀態,從其他訪問線程的角度看,修改不是已經完成就是還未開始。C++標準庫提供了很多類似的機制,最基本的就是互斥量,有一個< mutex >庫文件專門支持對共享數據結構的互斥訪問。
mutex 類是能用于保護共享數據免受從多個線程同時訪問的同步原語。
mutex 提供排他性非遞歸所有權語義:
- 調用方線程從它成功調用
lock或try_lock開始,到它調用unlock為止占有mutex。 - 線程占有
mutex時,所有其他線程若試圖要求mutex的所有權,則將阻塞(對于lock的調用)或收到 false 返回值(對于try_lock). - 調用方線程在調用
lock或try_lock前必須不占有mutex。
若 mutex 在仍為任何線程所占有時即被銷毀,或在占有 mutex 時線程終止,則行為未定義。 mutex 類滿足互斥體 (Mutex) 和標準布局類型 (StandardLayoutType) 的全部要求。
std::mutex 既不可復制亦不可移動。
-
互斥鎖類型是*可鎖定的類型,用于保護對代碼關鍵部分的訪問:鎖定互斥鎖可防止其他線程鎖定它(獨占訪問),直到被解鎖*為止:互斥體, recursive_mutex, timed_mutex, recursive_timed_mutex。
-
鎖是通過將互斥鎖與自己的生命周期相關聯的訪問來管理互斥鎖的對象:lock_guard, unique_lock。
-
同時鎖定多個互斥鎖的功能(try_lock, 鎖)并直接阻止并發執行特定功能(call_once)。
Mutex全名mutual exclusion(互斥體),是個object對象,用來協助采取獨占排他方式控制對資源的并發訪問。這里的資源可能是個對象,或多個對象的組合。**為了獲得獨占式的資源訪問能力,相應的線程必須鎖定(lock) mutex,這樣可以防止其他線程也鎖定mutex,直到第一個線程解鎖(unlock) mutex。**mutex類的主要操作函數見下表:
實例2:
| 操作 | 效果作用 | |
|---|---|---|
| 1 | mutex | 構造函數,建立一個未鎖定的(unlocked)mutex |
| 2 | m.~mutex | 銷毀mutex,它必須未被鎖定 |
| 3 | m.lock() | 嘗試鎖住mutex,它會造成阻塞 |
| 5 | m.try_lock() | 嘗試鎖住mutex,鎖定成功返回true |
| 6 | m.try_lock_for(dur) | 嘗試在時間段dur內鎖定,鎖定成功返回True |
| 7 | m.try_lock_until(tp) | 嘗試在時間點tp之前鎖定,鎖定成功返回True |
| 8 | m.unlock() | 解決mutex。如果它未曾被鎖定則行為不明確 |
| 9 | m.native_handle() | 返回一個因平臺而異的類型native_handle_type,為了不具有可移植性的拓展 |
// mutex1.cpp 通過互斥體lock與unlock保護共享全局變量#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100);std::mutex mutex;
int job_shared = 0; //兩個線程都能修改'job_shared',mutex將保護此變量
int job_exclusive = 0; //只有一個線程能修改'job_exclusive',不需要保護//此線程只能修改 'job_shared'
void job_1()
{mutex.lock();std::this_thread::sleep_for(5 * interval); //令‘job_1’持鎖等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";mutex.unlock();
}
// 此線程能修改'job_shared'和'job_exclusive'
void job_2()
{while (true) { //無限循環,直到獲得鎖并修改'job_shared'if (mutex.try_lock()) { //嘗試獲得鎖成功則修改'job_shared'++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";mutex.unlock();return;} else { //嘗試獲得鎖失敗,接著修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}
int main()
{std::thread thread_1(job_1);std::thread thread_2(job_2); thread_1.join();thread_2.join();getchar();return 0;
}
實例3:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::mutex mtx; // mutex for critical sectionvoid print_block(int n, char c) {// critical section (exclusive access to std::cout signaled by locking mtx):mtx.lock();for (int i = 0; i<n; ++i) { std::cout << c; }std::cout << '\n';mtx.unlock();
}int main()
{std::thread th1(print_block, 50, '*');std::thread th2(print_block, 50, '$');th1.join();th2.join();getchar();return 0;
}
從上代碼看,創建了兩個線程和兩個全局變量,其中一個全局變量job_exclusive是排他的,兩線程并不共享,不會產生數據競爭,所以不需要鎖保護。另一個全局變量job_shared是兩線程共享的,處于可讀寫的狀態,會引起數據競爭,因此需要鎖保護。線程thread_1持有互斥鎖lock的時間較長,線程thread_2為免于空閑等待,使用了嘗試鎖try_lock,如果獲得互斥鎖則操作共享變量job_shared,未獲得互斥鎖則操作排他變量job_exclusive,提高多線程效率。
可以看出持有鎖的線程可以對競爭的數據進行操作,所以,鎖的含義像一個令牌,持有令牌者,具有優先權限,其余線程需要等待令牌轉移,也即解鎖操作unlock。
注意
通常不直接使用 std::mutex : std::unique_lock 、 std::lock_guard 或 std::scoped_lock (C++17 起)以更加異常安全的方式管理鎖定。
3.3 C++11新標準多線程支持庫
- < thread > : 提供線程創建及管理的函數或類接口;
- < mutex > : 為線程提供獲得獨占式資源訪問能力的互斥算法,保證多個線程對共享資源的同步訪問;
- < condition_variable > : 允許一定量的線程等待(可以定時)被另一線程喚醒,然后再繼續執行;
- < future > : 提供了一些工具來獲取異步任務(即在單獨的線程中啟動的函數)的返回值,并捕捉其所拋出的異常;
- < atomic > : 為細粒度的原子操作(不能被處理器拆分處理的操作)提供組件,允許無鎖并發編程。
3.4 lock_guard與unique_lock保護共享資源*`
lock與unlock必須成對合理配合使用,使用不當可能會造成資源被永遠鎖住,甚至出現死鎖(兩個線程在釋放它們自己的lock之前彼此等待對方的lock)。是不是想起了C++另一對兒需要配合使用的對象new與delete,若使用不當可能會造成內存泄漏等嚴重問題,為此C++引入了智能指針shared_ptr與unique_ptr。智能指針借用了RAII技術(Resource Acquisition Is Initialization—使用類來封裝資源的分配和初始化,在構造函數中完成資源的分配和初始化,在析構函數中完成資源的清理,可以保證正確的初始化和資源釋放)對普通指針進行封裝,達到智能管理動態內存釋放的效果。同樣的,C++也針對lock與unlock引入了智能鎖lock_guard與unique_lock,同樣使用了RAII技術對普通鎖進行封裝,達到智能管理互斥鎖資源釋放的效果。
3.4.1 lock_guard:
類 lock_guard 是互斥體包裝器,為在作用域塊期間占有互斥提供便利 RAII 風格機制。
創建 lock_guard 對象時,它試圖接收給定互斥的所有權。控制離開創建 lock_guard 對象的作用域時,銷毀 lock_guard 并釋放互斥。
lock_guard 類不可復制。
實例4:
| 操作 | 作用 | |
|---|---|---|
| 1 | lock_guard lg(m) | 為mutex m建立一個lock guard并鎖定之 |
| 2 | lock_guard lg(m,adopt_lock) | 為已經被鎖定的mutex m建立一個lock guard |
| 3 | lg.~lock_guard() | 解鎖(unlock)mutex并銷毀lock_guard |
// lock_guard example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_errorstd::mutex mtx;void print_even(int x) {if (x % 2 == 0) std::cout << x << " is even\n";else throw (std::logic_error("not even"));
}void print_thread_id(int id) {try {// using a local lock_guard to lock mtx guarantees unlocking on destruction / exception://使用本地lock_guard鎖定mtx可以保證在銷毀/異常時解鎖:std::lock_guard<std::mutex> lck(mtx);print_even(id);}catch (std::logic_error&) {std::cout << "[exception caught]\n";}
}int main()
{std::thread threads[10];// spawn 10 threads:for (int i = 0; i<10; ++i)threads[i] = std::thread(print_thread_id, i + 1);for (auto& th : threads) th.join();getchar();return 0;
}
實例5:
#include <thread>
#include <mutex>
#include <iostream>int g_i = 0;
std::mutex g_i_mutex; // 保護 g_ivoid safe_increment()
{std::lock_guard<std::mutex> lock(g_i_mutex);++g_i;std::cout << std::this_thread::get_id() << ": " << g_i << '\n';// g_i_mutex 在鎖離開作用域時自動釋放
}int main()
{std::cout << "main: " << g_i << '\n';std::thread t1(safe_increment);std::thread t2(safe_increment);t1.join();t2.join();std::cout << "main: " << g_i << '\n';getchar();
}
采用RALL風格:使用類類封裝資源的分配和初始化, 控制離開創建 lock_guard 對象t1的作用域時,銷毀 lock_guard 并釋放互斥(持有lock鎖也即令牌者,擁有對資源的權限)。
3.4.2 unique_lock
類 unique_lock 是通用互斥包裝器,允許延遲鎖定、鎖定的有時限嘗試、遞歸鎖定、所有權轉移和與條件變量一同使用。
類 unique_lock 可移動,但不可復制——它滿足可移動構造 (MoveConstructible) 和可移動賦值 (MoveAssignable) 但不滿足可復制構造 (CopyConstructible) 或可復制賦值 (CopyAssignable) 。
類 unique_lock 滿足基本可鎖定 (BasicLockable) 要求。若 Mutex 滿足可鎖定 (Lockable) 要求,則 unique_lock 亦滿足可鎖定 (Lockable) 要求(例如:能用于 std::lock ) ;若 Mutex 滿足可定時鎖定 (TimedLockable) 要求,則 unique_lock 亦滿足可定時鎖定 (TimedLockable) 要求。
3.4.3 lock_guard與unique_lock的區別如下:
從上面兩個支持的操作函數表對比來看,unique_lock功能豐富靈活得多。如果需要實現更復雜的鎖策略可以用unique_lock,如果只需要基本的鎖功能,優先使用更嚴格高效的lock_guard。兩種鎖的概述與策略對比如下:
| 類模板 | 描述 | 策略 |
|---|---|---|
| std::lock_guard | 嚴格基于作用域(scope-based)的鎖管理類模板,構造時是否加鎖是可選的(不加鎖時假定當前線程已經獲得鎖的所有權—使用std::adopt_lock策略),析構時自動釋放鎖,所有權不可轉移,對象生存期內不允許手動加鎖和釋放鎖 | std::adopt_lock |
| std::unique_lock | 更加靈活的鎖管理類模板,構造時是否加鎖是可選的,在對象析構時如果持有鎖會自動釋放鎖,所有權可以轉移。對象生命期內允許手動加鎖和釋放鎖 | std::adopt_lock std::defer_lock std::try_to_lock |
實例6:
如果將上面實例2中針對job_1函數,如果普通鎖lock/unlock替換為智能鎖lock_guard,針對job_2的嘗試鎖try_lock也使用智能鎖替代,由于**lock_guard鎖策略不支持嘗試鎖,只好使用unique_lock來替代,**代碼修改如下:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100);std::mutex mutex;
int job_shared = 0; //兩個線程都能修改'job_shared',mutex將保護此變量
int job_exclusive = 0; //只有一個線程能修改'job_exclusive',不需要保護//此線程只能修改 'job_shared'
void job_1()
{mutex.lock();std::this_thread::sleep_for(5 * interval); //令‘job_1’持鎖等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";mutex.unlock();
}
void job_11()
{std::lock_guard<std::mutex> lockg(mutex); //獲取RAII智能鎖,離開作用域會自動析構解鎖std::this_thread::sleep_for(5 * interval); //令‘job_1’持鎖等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";
}
// 此線程能修改'job_shared'和'job_exclusive'
void job_2()
{while (true) { //無限循環,直到獲得鎖并修改'job_shared'if (mutex.try_lock()) { //嘗試獲得鎖成功則修改'job_shared'++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";mutex.unlock();return;}else { //嘗試獲得鎖失敗,接著修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}
void job_22()
{while (true) { //無限循環,直到獲得鎖并修改'job_shared'std::unique_lock<std::mutex> ulock(mutex, std::try_to_lock);//以嘗試鎖策略創建智能鎖//嘗試獲得鎖成功則修改'job_shared'if (ulock) {++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";return;}else { //嘗試獲得鎖失敗,接著修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}
int main()
{std::thread thread_1(job_11);std::thread thread_2(job_22);thread_1.join();thread_2.join();getchar();return 0;
}
3.5 timed_mutex與recursive_mutex提供更強大的鎖
3.3介紹的互斥量mutex提供了普通鎖lock/unlock和3.4介紹了智能鎖lock_guard/unique_lock,基本能滿足我們大多數對共享數據資源的保護需求。但在某些特殊情況下,我們需要更復雜的功能,比如某個線程中函數的嵌套調用可能帶來對某共享資源的嵌套鎖定需求,mutex在一個線程中卻只能鎖定一次;再比如我們想獲得一個鎖,但不想一直阻塞,只想等待特定長度的時間,mutex也沒提供可設定時間的鎖。針對這些特殊需求,< mutex >庫也提供了下面幾種功能更豐富的互斥類,它們間的區別見下表:
| 類模板 | 描述 |
|---|---|
| std::mutex | 同一時間只可被一個線程鎖定。如果它被鎖住,任何其他lock()都會阻塞(block),直到這個mutex再次可用,且try_lock()會失敗。 |
| std::recursive_mutex | 允許在同一時間多次被同一線程獲得其lock。其典型應用是:函數捕獲一個lock并調用另一函數而后者再次捕獲相同的lock。 |
| std::timed_mutex | 額外允許你傳遞一個時間段或時間點,用來定義多長時間內它可以嘗試捕獲一個lock。為此它提供了try_lock_for(duration)和try_lock_until(timepoint)。 |
| std::recursive_timed_mutex | 允許同一線程多次取得其lock,且可指定期限。 |
詳情見3.5鏈接。
實例7:
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> std::chrono::milliseconds interval(100);std::timed_mutex tmutex;int job_shared = 0; //兩個線程都能修改'job_shared',mutex將保護此變量
int job_exclusive = 0; //只有一個線程能修改'job_exclusive',不需要保護//此線程只能修改 'job_shared'
void job_13()
{std::lock_guard<std::timed_mutex> lockg(tmutex); //獲取RAII智能鎖,離開作用域會自動析構解鎖std::this_thread::sleep_for(5 * interval); //令‘job_1’持鎖等待++job_shared;std::cout << "job_1 shared (" << job_shared << ")\n";
}// 此線程能修改'job_shared'和'job_exclusive'
void job_23()
{while (true) { //無限循環,直到獲得鎖并修改'job_shared'std::unique_lock<std::timed_mutex> ulock(tmutex, std::defer_lock); //創建一個智能鎖但先不鎖定//嘗試獲得鎖成功則修改'job_shared'if (ulock.try_lock_for(3 * interval)) { //在3個interval時間段內嘗試獲得鎖++job_shared;std::cout << "job_2 shared (" << job_shared << ")\n";return;}else { //嘗試獲得鎖失敗,接著修改'job_exclusive'++job_exclusive;std::cout << "job_2 exclusive (" << job_exclusive << ")\n";std::this_thread::sleep_for(interval);}}
}int main()
{std::thread thread_1(job_13);std::thread thread_2(job_23);thread_1.join();thread_2.join();getchar();return 0;
}
4. 小結
本文由談到并發引發的數據競爭,以及數據競爭的初始解決方法,mutex互斥鎖解決方法(3.2),以及更高級的基于RALL的lock_guard與unique_lock智能鎖(3.4)(原因,防止mutex使用不當,造成死鎖)。3.5介紹一種更強大的鎖,用于線程中函數嵌套帶來的共享資源的嵌套鎖定需求,以及其它更復雜的情況。
//thread2.cpp 增加對cout顯示終端資源并發訪問的互斥鎖保護#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>using namespace std;std::mutex mutex1;void thread_function(int n)
{std::thread::id this_id = std::this_thread::get_id(); //獲取線程IDfor (int i = 0; i < 5; i++) {mutex1.lock();cout << "Child function thread " << this_id << " running : " << i + 1 << endl;mutex1.unlock();std::this_thread::sleep_for(std::chrono::seconds(n)); //進程睡眠n秒}
}class Thread_functor
{
public:// functor行為類似函數,C++中的仿函數是通過在類中重載()運算符實現,使你可以像使用函數一樣來創建類的對象void operator()(int n){std::thread::id this_id = std::this_thread::get_id();for (int i = 0; i < 5; i++) {{std::lock_guard<std::mutex> lockg(mutex1);cout << "Child functor thread " << this_id << " running: " << i + 1 << endl;}std::this_thread::sleep_for(std::chrono::seconds(n)); //進程睡眠n秒}}
};int main()
{thread mythread1(thread_function, 1); // 傳遞初始函數作為線程的參數if (mythread1.joinable()) //判斷是否可以成功使用join()或者detach(),返回true則可以,返回false則不可以mythread1.join(); // 使用join()函數阻塞主線程直至子線程執行完畢Thread_functor thread_functor;thread mythread2(thread_functor, 3); // 傳遞初始函數作為線程的參數if (mythread2.joinable())mythread2.detach(); // 使用detach()函數讓子線程和主線程并行運行,主線程也不再等待子線程auto thread_lambda = [](int n) {std::thread::id this_id = std::this_thread::get_id();for (int i = 0; i < 5; i++){mutex1.lock();cout << "Child lambda thread " << this_id << " running: " << i + 1 << endl;mutex1.unlock();std::this_thread::sleep_for(std::chrono::seconds(n)); //進程睡眠n秒}};thread mythread3(thread_lambda, 4); // 傳遞初始函數作為線程的參數if (mythread3.joinable())mythread3.join(); // 使用join()函數阻塞主線程直至子線程執行完畢unsigned int n = std::thread::hardware_concurrency(); //獲取可用的硬件并發核心數mutex1.lock();std::cout << n << " concurrent threads are supported." << endl;mutex1.unlock();std::thread::id this_id = std::this_thread::get_id();for (int i = 0; i < 5; i++) {{std::lock_guard<std::mutex> lockg(mutex1);cout << "Main thread " << this_id << " running: " << i + 1 << endl;}std::this_thread::sleep_for(std::chrono::seconds(1));}getchar();return 0;
}
參考資料:
https://zh.cppreference.com/w/cpp/thread/unique_lock
https://www.tutorialspoint.com/cpp_standard_library/memory.htm
https://www.cplusplus.com/reference/mutex/lock_guard/
總結
以上是生活随笔為你收集整理的【C++】多线程与互斥锁【二】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 引产需要多少钱啊?
- 下一篇: 【C++】多线程与条件变量【三】