C++?多線(xiàn)程之互斥量(mutex)詳解
C++ 11中的互斥量,聲明在 <mutex>
頭文件中,互斥量的使用可以在各種方面,比較常用在對(duì)共享數(shù)據(jù)的讀寫(xiě)上,如果有多個(gè)線(xiàn)程同時(shí)讀寫(xiě)一個(gè)數(shù)據(jù),那么想要保證多線(xiàn)程安全,就必須對(duì)共享變量的讀寫(xiě)進(jìn)行保護(hù)(上鎖),從而保證線(xiàn)程安全。
互斥量主要有四中類(lèi)型:
std::mutex
,最基本的 Mutex 類(lèi)。std::recursive_mutex
,遞歸 Mutex 類(lèi)。std::time_mutex
,限時(shí) Mutex 類(lèi)。std::recursive_timed_mutex
,限時(shí)遞歸 Mutex 類(lèi)。
當(dāng)然C++14和C++17各增加了一個(gè):
std::shared_timed_mutex
,限時(shí)讀寫(xiě)鎖(C++14)std::shared_mutex
,讀寫(xiě)鎖(C++17)
std::mutex
構(gòu)造函數(shù)
mutex(); mutex(const mutex&) = delete;
從上面的構(gòu)造函數(shù)可以看出,std::mutex不允許拷貝構(gòu)造,當(dāng)然也不允許move,最初構(gòu)造的mutex對(duì)象是處于未鎖定狀態(tài)的,若構(gòu)造不成功會(huì)拋出 std::system_error
。
析構(gòu)函數(shù)
~mutex();
銷(xiāo)毀互斥。若互斥被線(xiàn)程占有,或在占有mutex時(shí)線(xiàn)程被終止,則會(huì)產(chǎn)生未定義行為。
lock
void lock();
鎖定互斥,調(diào)用線(xiàn)程將鎖住該互斥量。線(xiàn)程調(diào)用該函數(shù)會(huì)發(fā)生下面 3 種情況:
- 如果該互斥量當(dāng)前沒(méi)有被其他線(xiàn)程鎖住,則調(diào)用線(xiàn)程將該互斥量鎖住,直到調(diào)用unlock之前,該線(xiàn)程一直擁有該鎖。
- 如果當(dāng)前互斥量被其他線(xiàn)程鎖住,則當(dāng)前的調(diào)用線(xiàn)程被阻塞住,指導(dǎo)其他線(xiàn)程unlock該互斥量。
- 如果當(dāng)前互斥量被當(dāng)前調(diào)用線(xiàn)程鎖住,則會(huì)產(chǎn)生死鎖(deadlock)。
try_lock
bool try_lock();
嘗試鎖住互斥量,立即返回。成功獲得鎖時(shí)返回 true ,否則返回 false。
如果互斥量被其他線(xiàn)程占有,則當(dāng)前線(xiàn)程也不會(huì)被阻塞。線(xiàn)程調(diào)用該函數(shù)也會(huì)出現(xiàn)下面 3 種情況:
- 如果當(dāng)前互斥量沒(méi)有被其他線(xiàn)程占有,則該線(xiàn)程鎖住互斥量,直到該線(xiàn)程調(diào)用 unlock 釋放互斥量。
- 如果當(dāng)前互斥量被其他線(xiàn)程鎖住,則當(dāng)前調(diào)用線(xiàn)程返回 false,而并不會(huì)被阻塞掉。
- 如果當(dāng)前互斥量被當(dāng)前調(diào)用線(xiàn)程鎖住,則會(huì)產(chǎn)生死鎖(deadlock)。
unlock
void unlock();
解鎖互斥?;コ饬勘仨殲楫?dāng)前執(zhí)行線(xiàn)程所鎖定(以及調(diào)用lock),否則行為未定義。
看下面一個(gè)簡(jiǎn)單的例子實(shí)現(xiàn)兩個(gè)線(xiàn)程競(jìng)爭(zhēng)全局變量g_num對(duì)其進(jìn)行寫(xiě)操作,然后打印輸出:
#include <iostream> #include <chrono> // std::chrono #include <thread> // std::thread #include <mutex> // std::mutex int g_num = 0; // 為 g_num_mutex 所保護(hù) std::mutex g_num_mutex; void slow_increment(int id) { for (int i = 0; i < 3; ++i) { g_num_mutex.lock(); ++g_num; std::cout << "th" << id << " => " << g_num << '\n'; g_num_mutex.unlock(); std::this_thread::sleep_for(std::chrono::seconds(1)); } } int main() { std::thread t1(slow_increment, 0); std::thread t2(slow_increment, 1); t1.join(); t2.join(); }
加了互斥量實(shí)現(xiàn)有序的寫(xiě)操作并輸出:
th0 => 1
th1 => 2
th0 => 3
th1 => 4
th1 => 5
th0 => 6
如果不增加mutex包含,可能輸出就不是有序的打印1到6,如下:
- thth01 => 2 => 2
- th1 => 3
- th0 => 4
- th0 => 5
- th1 => 6
std::recursive_mutex
如上面所說(shuō)的,如果使用std::mutex,如果一個(gè)線(xiàn)程在執(zhí)行中需要再次獲得鎖,會(huì)出現(xiàn)死鎖現(xiàn)象。要避免這種情況下就需要使用遞歸式互斥量std::recursive_mutex
,它不會(huì)產(chǎn)生上述的死鎖問(wèn)題,可以理解為同一個(gè)線(xiàn)程多次獲得鎖“僅僅增加鎖的計(jì)數(shù)”,同時(shí),必須要確保unlock和lock的次數(shù)相同,其他線(xiàn)程才可能取得這個(gè)mutex。它的接口與std::mutex的完全一樣,用法也基本相同除了可重入(必須同一線(xiàn)程才可重入,其他線(xiàn)程需等待),看下面的例子:
#include <iostream> #include <thread> #include <mutex> class X { std::recursive_mutex m; std::string shared; public: void fun1() { m.lock(); shared = "fun1"; std::cout << "in fun1, shared variable is now " << shared << '\n'; m.unlock(); } void fun2() { m.lock(); shared = "fun2"; std::cout << "in fun2, shared variable is now " << shared << '\n'; fun3(); // 遞歸鎖在此處變得有用 std::cout << "back in fun2, shared variable is " << shared << '\n'; m.unlock(); } void fun3() { m.lock(); shared = "fun3"; std::cout << "in fun3, shared variable is now " << shared << '\n'; m.unlock(); } }; int main() { X x; std::thread t1(&X::fun1, &x); std::thread t2(&X::fun2, &x); t1.join(); t2.join(); }
在fun2中調(diào)用fun3,而fun3中還使用了lock和unlock,只有遞歸式互斥量才能滿(mǎn)足當(dāng)前情況。
輸出如下:
in fun1, shared variable is now fun1
in fun2, shared variable is now fun2
in fun3, shared variable is now fun3
back in fun2, shared variable is fun3
std::time_mutex
timed_mutex增加了帶時(shí)限的try_lock。即try_lock_for
和try_lock_until
。
try_lock_for嘗試鎖互斥。阻塞直到超過(guò)指定的 timeout_duration
或得到鎖,取決于何者先到來(lái)。成功獲得鎖時(shí)返回 true,否則返回false 。函數(shù)原型如下:
template< class Rep, class Period > bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );
若timeout_duration
小于或等于timeout_duration.zero()
,則函數(shù)表現(xiàn)同try_lock()
。由于調(diào)度或資源爭(zhēng)議延遲,此函數(shù)可能阻塞長(zhǎng)于timeout_duration
。
#include <iostream> #include <sstream> #include <thread> #include <chrono> #include <vector> #include <mutex> std::timed_mutex mutex; using namespace std::chrono_literals; void do_work(int id) { std::ostringstream stream; for (int i = 0; i < 3; ++i) { if (mutex.try_lock_for(100ms)) { stream << "success "; std::this_thread::sleep_for(100ms); mutex.unlock(); } else { stream << "failed "; } std::this_thread::sleep_for(100ms); } std::cout << "[" << id << "] " << stream.str() << std::endl; } int main() { // try_lock_for std::vector<std::thread> threads; for (int i = 0; i < 4; ++i) { threads.emplace_back(do_work, i); } for (auto& t : threads) { t.join(); } }
[3] failed success failed
[0] success failed success
[2] failed failed failed
[1] success success success
try_lock_until也是嘗試鎖互斥。阻塞直至抵達(dá)指定的timeout_time
或得到鎖,取決于何者先到來(lái)。成功獲得鎖時(shí)返回 true,否則返回false。
timeout_time與上面的timeout_duration不一樣,timeout_duration表示一段時(shí)間,比如1秒,5秒或者10分鐘,而timeout_time表示一個(gè)時(shí)間點(diǎn),比如說(shuō)要等到8點(diǎn)30分或10點(diǎn)24分才超時(shí)。
使用傾向于timeout_time
的時(shí)鐘,這表示時(shí)鐘調(diào)節(jié)有影響。從而阻塞的最大時(shí)長(zhǎng)可能小于但不會(huì)大于在調(diào)用時(shí)的 timeout_time - Clock::now() ,依賴(lài)于調(diào)整的方向。由于調(diào)度或資源爭(zhēng)議延遲,函數(shù)亦可能阻塞長(zhǎng)于抵達(dá)timeout_time
之后。同try_lock()
,允許此函數(shù)虛假地失敗并返回false,即使在 timeout_time
前的某點(diǎn)任何線(xiàn)程都不鎖定互斥。函數(shù)原型如下:
template< class Clock, class Duration > bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time);
看下面的例子:
#include <iostream> #include <sstream> #include <thread> #include <chrono> #include <vector> #include <mutex> std::timed_mutex mutex; using namespace std::chrono; void do_work() { mutex.lock(); std::cout << "thread 1, sleeping..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(4)); mutex.unlock(); } void do_work2() { auto now = std::chrono::steady_clock::now(); if (mutex.try_lock_until(now + 5s)) { auto end = steady_clock::now(); std::cout << "try_lock_until success, "; std::cout << "time use: " << duration_cast<milliseconds>(end-now).count() << "ms." << std::endl; mutex.unlock(); } else { auto end = steady_clock::now(); std::cout << "try_lock_until failed, "; std::cout << "time use: " << duration_cast<milliseconds>(end-now).count() << "ms." << std::endl; } } int main() { // try_lock_until std::thread t1(do_work); std::thread t2(do_work2); t1.join(); t2.join(); }
獲得鎖時(shí)輸出:
thread 1, sleeping...
try_lock_until success, time use: 4000ms.
修改一下,讓其超時(shí),輸出:
thread 1, sleeping...
try_lock_until failed, time use: 5000ms.
std::recursive_timed_mutex
以類(lèi)似std::recursive_mutex的方式,recursive_timed_mutex
提供排他性遞歸鎖,同線(xiàn)程可以重復(fù)獲得鎖。另外,recursive_timed_mutex
通過(guò)try_lock_for
與try_lock_until
方法,提供帶時(shí)限地獲得recursive_timed_mutex
鎖,類(lèi)似std::time_mutex
。
std::shared_mutex
c++ 17 新出的具有獨(dú)占模式和共享模式的鎖。共享模式能夠被std::shared_lock
(這個(gè)后面再詳細(xì)將)占有。
std::shared_mutex 是讀寫(xiě)鎖,把對(duì)共享資源的訪(fǎng)問(wèn)者劃分成讀者和寫(xiě)者,讀者只對(duì)共享資源進(jìn)行讀訪(fǎng)問(wèn),寫(xiě)者則需要對(duì)共享資源進(jìn)行寫(xiě)操作。
它提供兩種訪(fǎng)問(wèn)權(quán)限的控制:共享性(shared)和排他性(exclusive)。通過(guò)lock/try_lock
獲取排他性訪(fǎng)問(wèn)權(quán)限(僅有一個(gè)線(xiàn)程能占有互斥),通過(guò)lock_shared/try_lock_shared
獲取共享性訪(fǎng)問(wèn)權(quán)限(多個(gè)線(xiàn)程能共享同一互斥的所有權(quán))。這樣的設(shè)置對(duì)于區(qū)分不同線(xiàn)程的讀寫(xiě)操作特別有用。
std::shared_mutex
通常用于多個(gè)讀線(xiàn)程能同時(shí)訪(fǎng)問(wèn)同一資源而不導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng),但只有一個(gè)寫(xiě)線(xiàn)程能訪(fǎng)問(wèn)的情形。比如,有多個(gè)線(xiàn)程調(diào)用shared_mutex.lock_shared()
,多個(gè)線(xiàn)程都可以獲得鎖,可以同時(shí)讀共享數(shù)據(jù),如果此時(shí)有一個(gè)寫(xiě)線(xiàn)程調(diào)用 shared_mutex.lock()
,則讀線(xiàn)程均會(huì)等待該寫(xiě)線(xiàn)程調(diào)用shared_mutex.unlock()
。對(duì)于C++11 沒(méi)有提供讀寫(xiě)鎖,可使用 boost::shared_mutex
。
std::shared_mutex
新增加的三個(gè)接口:
void lock_shared(); bool try_lock_shared(); void unlock_shared();
一個(gè)簡(jiǎn)單例子如下:
#include <iostream> #include <mutex> // 對(duì)于 std::unique_lock #include <shared_mutex> #include <thread> class ThreadSafeCounter { public: ThreadSafeCounter() = default; // 多個(gè)線(xiàn)程/讀者能同時(shí)讀計(jì)數(shù)器的值。 unsigned int get() const { std::shared_lock<std::shared_mutex> lock(mutex_); return value_; } // 只有一個(gè)線(xiàn)程/寫(xiě)者能增加/寫(xiě)線(xiàn)程的值。 void increment() { std::unique_lock<std::shared_mutex> lock(mutex_); value_++; } // 只有一個(gè)線(xiàn)程/寫(xiě)者能重置/寫(xiě)線(xiàn)程的值。 void reset() { std::unique_lock<std::shared_mutex> lock(mutex_); value_ = 0; } private: mutable std::shared_mutex mutex_; unsigned int value_ = 0; }; int main() { ThreadSafeCounter counter; auto increment_and_print = [&counter]() { for (int i = 0; i < 3; i++) { counter.increment(); std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n'; // 注意:寫(xiě)入 std::cout 實(shí)際上也要由另一互斥同步。省略它以保持示例簡(jiǎn)潔。 } }; std::thread thread1(increment_and_print); std::thread thread2(increment_and_print); thread1.join(); thread2.join(); } // 解釋?zhuān)合铝休敵鲈趩魏藱C(jī)器上生成。 thread1 開(kāi)始時(shí),它首次進(jìn)入循環(huán)并調(diào)用 increment() , // 隨后調(diào)用 get() 。然而,在它能打印返回值到 std::cout 前,調(diào)度器將 thread1 置于休眠 // 并喚醒 thread2 ,它顯然有足夠時(shí)間一次運(yùn)行全部三個(gè)循環(huán)迭代。再回到 thread1 ,它仍在首個(gè) // 循環(huán)迭代中,它最終打印其局部的計(jì)數(shù)器副本的值,即 1 到 std::cout ,再運(yùn)行剩下二個(gè)循環(huán)。 // 多核機(jī)器上,沒(méi)有線(xiàn)程被置于休眠,且輸出更可能為遞增順序。
可能的輸出:
139847802500864 1
139847802500864 2
139847802500864 3
139847794108160 4
139847794108160 5
139847794108160 6
std::shared_timed_mutex
它是從C++14 才提供的限時(shí)讀寫(xiě)鎖:std::shared_timed_mutex
。
對(duì)比std::shared_mutex
新增下面兩個(gè)接口,其實(shí)這兩個(gè)接口與上面講到的std::timed_mutex
的try_lock_for
和try_lock_until
類(lèi)似。都是限時(shí)等待鎖。只不過(guò)是增加了共享屬性。
template< class Rep, class Period > bool try_lock_shared_for( const std::chrono::duration<Rep,Period>& timeout_duration ); template< class Clock, class Duration > bool try_lock_shared_until( const std::chrono::time_point<Clock,Duration>& timeout_time );
總結(jié)
由于它們額外的復(fù)雜性,讀/寫(xiě)鎖std::shared_mutex
, std::shared_timed_mutex
優(yōu)于普通鎖std::mutex
,std::timed_mutex
的情況比較少見(jiàn)。但是理論上確實(shí)存在。
如果在頻繁但短暫的讀取操作場(chǎng)景,讀/寫(xiě)互斥不會(huì)提高性能。它更適合于讀取操作頻繁且耗時(shí)的場(chǎng)景。當(dāng)讀操作只是在內(nèi)存數(shù)據(jù)結(jié)構(gòu)中查找時(shí),很可能簡(jiǎn)單的鎖會(huì)勝過(guò)讀/寫(xiě)鎖。
如果讀取操作的開(kāi)銷(xiāo)非常大,并且您可以并行處理許多操作,那么在某些時(shí)候增加讀寫(xiě)比率應(yīng)該會(huì)導(dǎo)致讀取/寫(xiě)入器性能優(yōu)于排他鎖的情況。斷點(diǎn)在哪里取決于實(shí)際工作量。
另請(qǐng)注意,在持有鎖的同時(shí)執(zhí)行耗時(shí)的操作通常是一個(gè)壞兆頭??赡苡懈玫姆椒▉?lái)解決問(wèn)題,然后使用讀/寫(xiě)鎖。
還要注意,在使用mutex時(shí),要時(shí)刻注意lock()與unlock()的加鎖臨界區(qū)的范圍,不能太大也不能太小,太大了會(huì)導(dǎo)致程序運(yùn)行效率低下,大小了則不能滿(mǎn)足我們對(duì)程序的控制。并且我們?cè)诩渔i之后要及時(shí)解鎖,否則會(huì)造成死鎖,lock()與unlock()應(yīng)該是成對(duì)出現(xiàn)。
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
一篇文章徹底弄懂C++虛函數(shù)的實(shí)現(xiàn)機(jī)制
C++中的虛函數(shù)的作用主要是實(shí)現(xiàn)了多態(tài)的機(jī)制,基類(lèi)定義虛函數(shù),子類(lèi)可以重寫(xiě)該函數(shù),在派生類(lèi)中對(duì)基類(lèi)定義的虛函數(shù)進(jìn)行重寫(xiě)時(shí),需要在派生類(lèi)中聲明該方法為虛方法,這篇文章主要給大家介紹了關(guān)于如何通過(guò)一篇文章徹底弄懂C++虛函數(shù)的實(shí)現(xiàn)機(jī)制,需要的朋友可以參考下2021-06-06C++ 中時(shí)間與時(shí)間戳的轉(zhuǎn)換實(shí)例詳解
這篇文章主要介紹了C++ 中時(shí)間與時(shí)間戳的轉(zhuǎn)換實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-06-06C/C++ 中堆和棧及靜態(tài)數(shù)據(jù)區(qū)詳解
這篇文章主要介紹了C/C++ 中堆和棧及靜態(tài)數(shù)據(jù)區(qū)詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04C語(yǔ)言實(shí)現(xiàn)圖的最短路徑Floyd算法
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)圖的最短路徑Floyd算法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01C++之CNoTrackObject類(lèi)和new delete操作符的重載實(shí)例
這篇文章主要介紹了C++之CNoTrackObject類(lèi)和new delete操作符的重載實(shí)例,是C++程序設(shè)計(jì)中比較重要的概念,需要的朋友可以參考下2014-10-10