關(guān)于線程同步與Mutex的使用
一、什么是線程同步?
- 線程同步(Thread Synchronization)是多線程編程中的一個(gè)重要概念,它指的是通過(guò)一定的機(jī)制來(lái)控制多個(gè)線程之間的執(zhí)行順序,以確保它們能夠正確地訪問(wèn)和修改共享資源,從而避免數(shù)據(jù)競(jìng)爭(zhēng)和不一致性問(wèn)題。
- 在多線程環(huán)境中,多個(gè)線程可能同時(shí)訪問(wèn)和修改共享資源(如變量,數(shù)據(jù)結(jié)構(gòu)或文件等)。如果沒(méi)有適當(dāng)?shù)耐綑C(jī)制,這些線程可能會(huì)以不可預(yù)測(cè)的順序執(zhí)行,導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)、臟讀、臟寫(xiě)或其他不可預(yù)期的行為。線程同步的目標(biāo)就是確保線程之間的有序執(zhí)行,以維護(hù)數(shù)據(jù)的一致性和完整性。
為什么要同步呢?
如果在同一時(shí)刻,僅只有一個(gè)線程訪問(wèn)某個(gè)變量(臨界資源),不會(huì)存在臟數(shù)據(jù)的問(wèn)題,但是,如果同一時(shí)刻有多個(gè)線程,同時(shí)訪問(wèn)同一個(gè)臨界資源的時(shí)候,就會(huì)存在 問(wèn)題,如何來(lái)解決這個(gè)問(wèn)題呢?這就提出了“線程同步”這個(gè)概念。
以下是一個(gè)簡(jiǎn)單的例子:
那么我們有沒(méi)有什么辦法,控制這些線程對(duì)臨界資源的訪問(wèn)呢?想個(gè)什么辦法才能使它們之間不亂套呢?這就該線程同步機(jī)制登場(chǎng)了
二、線程同步機(jī)制
C++中提供了多種線程同步機(jī)制,常用的方法包括:
- 互斥鎖(Mutex):互斥鎖是最常用的線程同步機(jī)制之一,當(dāng)一個(gè)線程想要訪問(wèn)共享資源時(shí),它首先會(huì)嘗試獲取與該資源關(guān)聯(lián)的互斥鎖如果鎖已經(jīng)被其他線程持有,則該線程將被阻塞,直到鎖被釋放。這樣可以確保在任何時(shí)候只有一個(gè)線程能夠訪問(wèn)共享資源。
- 條件變量(Condition Variable):條件變量用于使線程在滿(mǎn)足某個(gè)條件之前等待,它通常與互斥鎖一起使用,以便在等待條件成立時(shí)釋放鎖,并在條件成立時(shí)重新獲取鎖。這允許線程在等待期間不占用鎖,從而提高并發(fā)性能。
- 信號(hào)量(Semaphore):信號(hào)量是一種通用的線程同步機(jī)制,它允許多個(gè)線程同時(shí)訪問(wèn)共享資源,但限制同時(shí)訪問(wèn)的線程數(shù)量。信號(hào)量?jī)?nèi)部維護(hù)一個(gè)計(jì)數(shù)器,用于表示可用資源的數(shù)量。當(dāng)線程需要訪問(wèn)資源時(shí),它會(huì)嘗試減少計(jì)數(shù)器的值;當(dāng)線程釋放資源時(shí),它會(huì)增加計(jì)數(shù)器的值。當(dāng)計(jì)數(shù)器的值小于零時(shí),嘗試獲取資源的線程將被阻塞。
- 原子操作(Atomic Operation):原子操作時(shí)不可中斷的操作,即在執(zhí)行過(guò)程中不會(huì)被其他線程打斷。C++11及以后的版本提供了頭文件,其中包含了一系列原子操作的函數(shù)和類(lèi)。這些原子操作可以用于安全地更新共享數(shù)據(jù),而無(wú)需是同互斥鎖等同步機(jī)制。
三、互斥鎖(Mutex)
互斥(Mutex)是一種同步機(jī)制,同于保護(hù)共享資源,放置多個(gè)線程同時(shí)訪問(wèn)和修改同一資源,從而引起數(shù)據(jù)競(jìng)爭(zhēng)(data race)和不一致性。
當(dāng)一個(gè)線程想要訪問(wèn)某個(gè)共享資源時(shí),它首先會(huì)嘗試獲取與該資源關(guān)聯(lián)的互斥鎖(mutex)。如果互斥鎖已經(jīng)被其他線程持有(即被鎖定),則該線程將被阻塞,直到互斥鎖被釋放(即被解鎖)。一旦線程成功獲取到互斥鎖,它就可以安全地訪問(wèn)共享資源,并在訪問(wèn)完成后釋放互斥鎖,以便其他線程可以獲取該鎖并訪問(wèn)資源。
互斥鎖通常具有以下幾個(gè)特性:
- 互斥性:任意時(shí)刻只有一個(gè)線程可以持有某個(gè)互斥鎖。
- 原子性:對(duì)互斥鎖的獲取和釋放操作時(shí)原子的,即在執(zhí)行這些操作時(shí)不會(huì)被其他線程打斷。
- 可重入性:某些互斥鎖類(lèi)型(如遞歸鎖)允許同一線程多次獲取同一個(gè)鎖,但通常不建議這樣做,因?yàn)樗鼤?huì)增加死鎖的風(fēng)險(xiǎn)。
- 非阻塞性:雖然互斥鎖本身是一種阻塞性同步機(jī)制,但某些高級(jí)實(shí)現(xiàn)(如嘗試鎖)允許線程在無(wú)法立即獲取鎖時(shí)繼續(xù)執(zhí)行其他任務(wù),而不是被阻塞。
#include <iostream> #include <thread> using namespace std; //共享變量,沒(méi)有互斥鎖或原子操作 int counter = 0; //線程函數(shù) 對(duì)counter進(jìn)行自增操作 void increment_counter(int times) { for(int i=0;i<times;++i) { //這是一個(gè)數(shù)據(jù)競(jìng)爭(zhēng),因?yàn)槎鄠€(gè)線程可能同時(shí)執(zhí)行這行代碼 counter++; } } int main() { //創(chuàng)建兩個(gè)線程,每個(gè)線程對(duì)counter自增100000次 thread t1(increment_counter,100000); thread t2(increment_counter,100000); //等待兩個(gè)線程完成 t1.join(); t2.join(); //輸出結(jié)果,這個(gè)結(jié)果可能不是200000 cout<<"最終結(jié)果:"<<counter<<endl; return 0; }
在以上的這個(gè)例子中,輸出的結(jié)果可能不是我們想要的那個(gè)答案,這是因?yàn)槲覀儎?chuàng)建了兩個(gè)線程,這兩個(gè)線程的同時(shí)占用counter這個(gè)資源,前一個(gè)線程剛修改counter的值,后一個(gè)線程可能就會(huì)立即覆蓋掉當(dāng)前這個(gè)counter的值。
接下來(lái)我們對(duì)這個(gè)例子進(jìn)行改造:
#include <iostream> #include <thread> #include <mutex> using namespace std; //共享變量,沒(méi)有互斥鎖或原子操作 int counter = 0; //定義一個(gè)互斥鎖 mutex mymutex; //線程函數(shù) 對(duì)counter進(jìn)行自增操作 void increment_counter(int times) { for(int i=0;i<times;++i) { mymutex.lock();//在訪問(wèn)臨界資源之前先加鎖 counter++; mymutex.unlock();//訪問(wèn)完了之后解鎖,把鎖釋放 } } int main() { //創(chuàng)建兩個(gè)線程,每個(gè)線程對(duì)counter自增100000次 thread t1(increment_counter,100000); thread t2(increment_counter,100000); //等待兩個(gè)線程完成 t1.join(); t2.join(); //輸出結(jié)果 cout<<"最終結(jié)果:"<<counter<<endl; return 0; }
通過(guò)加上互斥鎖之后,我們輸出的結(jié)果將是正確的。
四、loock 和 unlock
在C++中,使用std::mutex的lock()和unlock()函數(shù)來(lái)管理對(duì)共享資源的訪問(wèn),從而確保在多線程環(huán)境中資源的同步訪問(wèn)。
以下是關(guān)于如何使用它們以及需要注意的事項(xiàng):
1.如何使用?
首先,你需要?jiǎng)?chuàng)建一個(gè)std::mutex對(duì)象
std::mutex mymutex;
在訪問(wèn)共享資源之前,使用lock()函數(shù)鎖定互斥量
mymutex.lock(); //訪問(wèn)共享變量 ...............
在互斥鎖被鎖定期間,你可以安全的訪問(wèn)共享資源,因?yàn)槠渌噲D鎖定該互斥量的線程將被阻塞。
一旦完成對(duì)共享資源的訪問(wèn),將會(huì)使用unlock()函數(shù)解鎖互斥量。
//完成對(duì)共享資源的訪問(wèn) mymutex.unlock();
2.注意事項(xiàng)
(1)死鎖:
如果線程在持有互斥量的情況下調(diào)用了一個(gè)阻塞操作(如另一個(gè)互斥量的lock()),并且這個(gè)阻塞操作永遠(yuǎn)不會(huì)完成(因?yàn)槠渌€程持有它需要的資源),那么就會(huì)發(fā)生死鎖,避免死鎖的一種方法就是始終按照相同的順序鎖定互斥量,或者使用更高級(jí)的同步原語(yǔ),如std::lock_guard或std::unique_lock,它們可以自動(dòng)管理鎖的獲取和釋放。
(2)異常安全:
如果在鎖定互斥量之后拋出異常,那么必須確定互斥量被正確解鎖,使用std::lock_guard或std::unique_lock可以自動(dòng)處理這種情況,因?yàn)樗鼈冊(cè)谖鰳?gòu)時(shí)會(huì)釋放鎖。
如下面這個(gè)例子:
#include <iostream> #include <thread> #include <mutex> using namespace std; //全局互斥量 mutex mymutex; void safe_function() { std::lock_guard<mutex> lock(mymutex);//鎖定互斥量 //在這里執(zhí)行需要互斥訪問(wèn)的代碼 //如果拋出異常,lock_guard會(huì)在析構(gòu)時(shí)自動(dòng)解鎖mymutex try{ //模擬一些可能拋出的異常 if(/*some condition that might cause an exception*/){ throw std::runtime_error("An error occurred");//使用拋出異常 } //......其他代碼塊 } catch(const std::exception& e){ //處理異常,但不需要擔(dān)心解鎖,因?yàn)閘ock_guard會(huì)自動(dòng)處理 std::cerr << "Caught exception:"<<e.what() << '\n'; } //lock_guard離開(kāi)作用域時(shí)自動(dòng)解鎖mymutex } int main() { //假設(shè)這里有一些線程調(diào)用safe_function() //由于使用了lock_guard,所以wu'lun是否拋出異常,mymutex都會(huì)被正確解鎖 //........ return 0; }
(3)不要手動(dòng)解鎖未鎖定的互斥量:
在調(diào)用unlock()之前,必須確保互斥量已經(jīng)被lock()鎖定,否則,該行為是未定義的。
(4)不要多次鎖定同一互斥量:
對(duì)于非遞歸互斥量(如std::mutex),不要再同一線程中多次鎖定它。這會(huì)導(dǎo)致未定義的行為。如果需要遞歸鎖定,請(qǐng)使用std::recursive_mutex。
(5)使用RAII管理鎖
使用RAII(資源獲取即初始化)原則來(lái)管理鎖的生命周期,通過(guò)std::lock_guardhuostd::unique_lock來(lái)確保鎖在不需要時(shí)自動(dòng)釋放。
(6)避免長(zhǎng)時(shí)間持有鎖
盡量縮短持有鎖的時(shí)間,以減少線程之間的爭(zhēng)用,提高程序的并發(fā)性能。
(7)考慮使用更高級(jí)的同步原語(yǔ)
除了std::mutex之外,C++標(biāo)準(zhǔn)庫(kù)還提供了其他更高級(jí)的同步原語(yǔ),如條件變量(std::condition_variable)、讀寫(xiě)鎖(std::shared_mutex)等,它們可以在特定場(chǎng)景下提供更高效的同步機(jī)制。
五、Mutex的四種類(lèi)型
在C++中,特別是從C++11開(kāi)始,std::mutex及其相關(guān)類(lèi)型提供了一系列用于同步多線程訪問(wèn)共享資源的機(jī)制,以下時(shí)std::mutex的四種主要類(lèi)型及其詳細(xì)解釋?zhuān)?/p>
1.std::mutex
- 這是最基本的互斥量類(lèi)型
- 它不允許遞歸鎖頂,即同一個(gè)線程不能被多次鎖定同一個(gè)std::mutex,如果嘗試這樣做,程序的行為將是未定義的,通常會(huì)導(dǎo)致死鎖。
- 它提供了基本的鎖定(lock)和解鎖(unlock)操作
- 當(dāng)一個(gè)線程鎖定了一個(gè)std::mutex時(shí),任何其他嘗試鎖定該互斥量的線程都將被阻塞,直到原始線程調(diào)用unlock()釋放它。
2.std::recursive_mutex
- 這是一個(gè)遞歸(或可重入)互斥量
- 與std::mutex不同,它允許同一線程多次鎖定一個(gè)互斥量。這可以用于需要遞歸訪問(wèn)受保護(hù)資源的場(chǎng)景。
- 線程在每次鎖定時(shí)都需要對(duì)應(yīng)的解鎖,以確保正確的同步。
- 如果線程沒(méi)有正確匹配其鎖定和解鎖操作(即解鎖次數(shù)少于鎖定次數(shù)),則其他線程仍然會(huì)被阻塞。
#include <iostream> #include <thread> #include <mutex> using namespace std; std::recursive_mutex mtx; void recursive_function() { mtx.lock();//第一次鎖定 cout<<"Thread "<<this_thread::get_id()<<" locked mutex"<<endl; //遞歸鎖定 mtx.lock();//同一線程可以多次鎖定 cout<<"Thread "<<this_thread::get_id()<<" locked mutex again"<<endl; mtx.unlock();//解鎖一次 cout<<"Thread "<<this_thread::get_id()<<" unlocked mutex"<<endl; mtx.unlock();//再次解鎖 cout<<"Thread "<<this_thread::get_id()<<" unlocked mutex again"<<endl; } int main() { std::thread t1(recursive_function); t1.join(); return 0; }
3.std::timed_mutex
- 這是一個(gè)帶時(shí)限的互斥量
- 除了提供基本的鎖定和解鎖操作外,它還允許線程嘗試在一定時(shí)間內(nèi)鎖定互斥量。
- 如果在指定時(shí)間內(nèi)無(wú)法獲取鎖,try_lock_for()或try_lock_until()函數(shù)將返回失敗,而線程則不會(huì)被阻塞。
- 這對(duì)于實(shí)現(xiàn)有超時(shí)機(jī)制的資源訪問(wèn)非常有用。
#include <iostream> #include <thread> #include <mutex> using namespace std; std::timed_mutex mtx; void timed_lock_function() { auto start = std::chrono::high_resolution_clock::now();//高精度時(shí)間 //嘗試在指定時(shí)間內(nèi)獲取鎖 if(mtx.try_lock_for(std::chrono::seconds(2)))//加了一個(gè)2秒的等待時(shí)間 { cout<<"Thread"<<this_thread::get_id()<<" get the lock"<<endl; std::this_thread::sleep_for(std::chrono::seconds(3)); mtx.unlock(); cout<<"Thread"<<this_thread::get_id()<<" release the lock"<<endl; } else { cout<<"Thread"<<this_thread::get_id()<<" can't get the lock"<<endl; } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> diff = end-start; cout<<"Thread"<<this_thread::get_id()<<" cost "<<diff.count()<<"s"<<endl; } int main() { std::thread t1(timed_lock_function); std::thread t2(timed_lock_function); t1.join(); t2.join(); return 0; }
4.std::recursive_timed_mutex:
- 這是以惡搞遞歸且?guī)r(shí)限的互斥量
- 它結(jié)合了std::recursive_mutex和std::timed_mutex的特性
- 它允許同一線程多次鎖定同一個(gè)互斥量,并提供了帶時(shí)限的鎖定嘗試功能。
- 這使得線程在需要遞歸訪問(wèn)資源且希望在一定時(shí)間內(nèi)獲取鎖的場(chǎng)景中更加靈活。
在使用這些互斥量類(lèi)型時(shí),需要注意正確的管理鎖定和解鎖操作,以避免死鎖和其他同步問(wèn)題。同時(shí),根據(jù)具體的應(yīng)用場(chǎng)景和需求選擇合適的互斥量類(lèi)型也是非常重要的。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
va_list(),va_start(),va_arg(),va_end() 詳細(xì)解析
這些宏定義在stdarg.h中,所以用到可變參數(shù)的程序應(yīng)該包含這個(gè)頭文件.下面我們寫(xiě)一個(gè)簡(jiǎn)單的可變參數(shù)的函數(shù),該函數(shù)至少有一個(gè)整數(shù)參數(shù),第二個(gè)參數(shù)也是整數(shù),是可選的.函數(shù)只是打印這兩個(gè)參數(shù)的值2013-09-09C++中fstream,ifstream及ofstream用法淺析
這篇文章主要介紹了C++中fstream,ifstream及ofstream用法,適合C++初學(xué)者學(xué)習(xí)文件流的操作,需要的朋友可以參考下2014-08-08一文詳解C語(yǔ)言中文件相關(guān)函數(shù)的使用
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言中文件相關(guān)函數(shù)的使用,可以實(shí)現(xiàn)文件的讀寫(xiě)、打開(kāi)和關(guān)閉。文中通過(guò)示例進(jìn)行了詳細(xì)介紹,需要的可以參考一下2022-07-07