C++多線程之互斥鎖與死鎖
1.前言
比如說(shuō)我們現(xiàn)在以一個(gè)list容器來(lái)模仿一個(gè)消息隊(duì)列,當(dāng)消息來(lái)臨時(shí)插入list的尾部,當(dāng)讀取消息時(shí)就把頭部的消息讀出來(lái)并且刪除這條消息。在代碼中就以?xún)蓚€(gè)線程分別實(shí)現(xiàn)消息寫(xiě)入和消息讀取的功能,如下:
class msgList { private: list<int>mylist; //用list模仿一個(gè)消息隊(duì)列 public: void WriteList() //向消息隊(duì)列中寫(xiě)入消息(以i作為消息) { for (int i = 0; i<100000; i++) { cout << "Write : " << i <<endl; mylist.push_back(i); } return; } void ReadList() //從消息隊(duì)列中讀取并取出消息 { for(int i=0;i<100000;i++) { if (!mylist.empty()) { cout << "Read : " << mylist.front() << endl; mylist.pop_front(); } else { cout << "Message List is empty!" << endl; } } } }; int main() { msgList mlist; thread pread(&msgList::ReadList, &mlist); //讀線程 thread pwrite(&msgList::WriteList, &mlist); //寫(xiě)線程 //等待線程結(jié)束 pread.join(); pwrite.join(); return 0; }
這段程序在運(yùn)行過(guò)程中,大部分時(shí)間是正常的,但是也會(huì)出現(xiàn)如下不穩(wěn)定的情況:
為什么會(huì)出現(xiàn)這種情況呢?
這是因?yàn)橄㈥?duì)列對(duì)于讀線程和寫(xiě)線程來(lái)說(shuō)是共享的,這時(shí)就會(huì)出現(xiàn)兩種特殊的情況:讀線程的讀取操作還沒(méi)有結(jié)束,線程上下文就切換到了寫(xiě)線程中;或者寫(xiě)線程的寫(xiě)入操作還沒(méi)有結(jié)束,線程上下文切換就到了讀線程中,這兩種情況都反映了讀寫(xiě)沖突,從而出現(xiàn)了以上錯(cuò)誤。
要想解決這個(gè)問(wèn)題,最顯然最直接的方法就是將讀寫(xiě)操作分離開(kāi)來(lái),讀的時(shí)候不允許寫(xiě),寫(xiě)的時(shí)候不允許讀,這樣,才能實(shí)現(xiàn)線程安全的讀和寫(xiě)。說(shuō)形象一點(diǎn),就是在進(jìn)行讀操作時(shí),就對(duì)共享資源進(jìn)行加鎖,禁止其他線程訪問(wèn),其他線程要訪問(wèn)就得等到讀線程解鎖才行,就像上廁所一樣,一次只能上一個(gè)人,其他人必須得等他上完了再上。這樣,就有了互斥鎖的概念。
2.互斥鎖
在多任務(wù)操作系統(tǒng)中,同時(shí)運(yùn)行的多個(gè)任務(wù)可能都需要使用同一種資源。比如說(shuō),同一個(gè)文件,可能一個(gè)線程會(huì)對(duì)其進(jìn)行寫(xiě)操作,而另一個(gè)線程需要對(duì)這個(gè)文件進(jìn)行讀操作,可想而知,如果寫(xiě)線程還沒(méi)有寫(xiě)結(jié)束,而此時(shí)讀線程開(kāi)始了,或者讀線程還沒(méi)有讀結(jié)束而寫(xiě)線程開(kāi)始了,那么最終的結(jié)果顯然會(huì)是混亂的。為了保護(hù)共享資源,在線程里也有這么一把鎖——互斥鎖(mutex),互斥鎖是一種簡(jiǎn)單的加鎖的方法來(lái)控制對(duì)共享資源的訪問(wèn),互斥鎖只有兩種狀態(tài),即上鎖( lock )和解鎖( unlock )。
2.1 互斥鎖的特點(diǎn)
1. 原子性:把一個(gè)互斥量鎖定為一個(gè)原子操作,這意味著如果一個(gè)線程鎖定了一個(gè)互斥量,沒(méi)有其他線程在同一時(shí)間可以成功鎖定這個(gè)互斥量;
2. 唯一性:如果一個(gè)線程鎖定了一個(gè)互斥量,在它解除鎖定之前,沒(méi)有其他線程可以鎖定這個(gè)互斥量;
3. 非繁忙等待:如果一個(gè)線程已經(jīng)鎖定了一個(gè)互斥量,第二個(gè)線程又試圖去鎖定這個(gè)互斥量,則第二個(gè)線程將被掛起(不占用任何cpu資源),直到第一個(gè)線程解除對(duì)這個(gè)互斥量的鎖定為止,第二個(gè)線程則被喚醒并繼續(xù)執(zhí)行,同時(shí)鎖定這個(gè)互斥量。
2.2 互斥鎖的使用
根據(jù)前面我們可以知道,互斥鎖主要就是用來(lái)保護(hù)共享資源的,在C++ 11中,互斥鎖封裝在mutex類(lèi)中,通過(guò)調(diào)用類(lèi)成員函數(shù)lock()和unlock()來(lái)實(shí)現(xiàn)加鎖和解鎖。值得注意的是,加鎖和解鎖,必須成對(duì)使用,這也是比較好理解的。除此之外,互斥量的使用時(shí)機(jī),就以開(kāi)篇程序?yàn)槔覀円Wo(hù)的共享資源當(dāng)然就是消息隊(duì)列l(wèi)ist了,那么互斥鎖應(yīng)該加在哪里呢?
可能想的比較簡(jiǎn)單一點(diǎn):就直接把鎖加在函數(shù)最前面不就好了么?如下所示:
class msgList { private: list<int>mylist; //用list模仿一個(gè)消息隊(duì)列 mutex mtx; //創(chuàng)建互斥鎖對(duì)象 public: void WriteList() //向消息隊(duì)列中寫(xiě)入消息(以i作為消息) { mtx.lock(); for (int i = 0; i<100000; i++) { cout << "Write : " << i <<endl; mylist.push_back(i); } mtx.unlock(); return; } //....... };
不過(guò)如果這樣加鎖的話(huà),要等寫(xiě)線程完全執(zhí)行結(jié)束才能開(kāi)始讀線程,讀寫(xiě)線程變成了串行執(zhí)行,這就違背了線程并發(fā)性的特點(diǎn)了。正確的加鎖方式應(yīng)當(dāng)是在執(zhí)行寫(xiě)操作的具體部分加鎖,如下所示:
class msgList { private: list<int>mylist; //用list模仿一個(gè)消息隊(duì)列 mutex mtx; //創(chuàng)建互斥鎖對(duì)象 public: void WriteList() //向消息隊(duì)列中寫(xiě)入消息(以i作為消息) { for (int i = 0; i<100000; i++) { mtx.lock(); cout << "Write : " << i <<endl; mylist.push_back(i); mtx.unlock(); } return; } //....... };
這樣,才能真正的實(shí)現(xiàn)讀寫(xiě)互不干擾。
下面再舉一個(gè)更為直觀的例子,創(chuàng)建兩個(gè)線程同時(shí)對(duì)list進(jìn)行寫(xiě)操作:
class msgList { private: list<int>mylist; mutex m; int i = 0; public: void WriteList() { while(i<1000) { mylist.push_back(i++); } return; } void showList() { for (auto p = mylist.begin(); p != mylist.end(); p++) { cout << (*p) << " "; } cout << endl; cout << "size of list : " << mylist.size() << endl; return; } }; int main() { msgList mlist; thread pwrite0(&msgList::WriteList, &mlist); thread pwrite1(&msgList::WriteList, &mlist); pwrite0.join(); pwrite1.join(); cout << "threads end!" << endl; mlist.showList(); //子線程結(jié)束后主線程打印list return 0; }
這里用兩個(gè)線程來(lái)寫(xiě)list,并且最終在主線程中調(diào)用了showList()來(lái)輸出list的size和所有元素,我們先來(lái)看下輸出情況:
根據(jù)結(jié)果可以看到,這里有很多問(wèn)題:實(shí)際輸出的元素個(gè)數(shù)和size不符,輸出的元素也并不是連續(xù)的,這都是多個(gè)線程同時(shí)更新list所造成的情況。這種情況下,運(yùn)行結(jié)果是無(wú)法預(yù)料的,每次都可能不一樣。這就是線程不安全所引發(fā)的問(wèn)題,我們加上鎖再來(lái)看看:
class msgList { private: list<int>mylist; mutex m; int i = 0; public: void WriteList() { while(i<1000) { m.lock();//加鎖 mylist.push_back(i++); m.unlock(); //解鎖 } return; } // ...... };
這樣加鎖就正確了嗎?我們?cè)俣噙\(yùn)行幾次看看:
數(shù)字都是連續(xù)的,但是個(gè)數(shù)卻多了一個(gè)(出現(xiàn)的幾率還是比較?。?,這又是什么原因造成的呢?還是兩個(gè)線程的問(wèn)題,假設(shè)要插入1000個(gè)數(shù),循環(huán)條件就是while(i<1000),當(dāng)i=999的時(shí)候兩個(gè)寫(xiě)線程都可以進(jìn)入while循環(huán),此時(shí)如果pwrite0線程拿到了lock(),那么pwrite1線程就只能一直等待,pwrite0線程繼續(xù)往下執(zhí)行,使得i變成了1000,此時(shí),對(duì)于pwrite0線程來(lái)說(shuō),它就必須退出循環(huán)了。而此時(shí)的pwrite1在哪里呢?還等在lock()的地方,pwrite0線程unlock()后,pwrite1成功lock(),此時(shí)i=1000,但是pwrite1卻還沒(méi)有執(zhí)行完此次循環(huán),因此向list中插入1000,此時(shí)退出的i的值為1001,這也就造成了實(shí)際輸出為1001個(gè)數(shù)的情況。
為了避免這個(gè)問(wèn)題,一個(gè)簡(jiǎn)單的辦法就是在lock()之后再加上一個(gè)判斷,判斷i是否依舊滿(mǎn)足while的條件,如下:
void WriteList() { while(i<10000) { m.lock(); if (i >= 10000) { m.unlock(); //退出之前必須先解鎖 break; } mylist.push_back(i++); m.unlock(); } return; }
為什么這里要在break前面加一個(gè)unlock()呢?原因就在于:如果break前面沒(méi)有unlock(),一旦i符合了if的條件,就直接break了,此時(shí)就沒(méi)法unlock(),程序就會(huì)報(bào)錯(cuò):
可以發(fā)現(xiàn),這種錯(cuò)誤是比較難發(fā)現(xiàn)的,特別是像這樣程序中出現(xiàn)了分支的情況,很容易就使得程序?qū)嶋H運(yùn)行時(shí)lock()了卻沒(méi)有unclock()。為了解決這一問(wèn)題,就有了std::lock_guard。
2.3 std::lock_guard
簡(jiǎn)單來(lái)理解的話(huà),lock_guard就是一個(gè)類(lèi),它會(huì)在其構(gòu)造函數(shù)中加鎖,而在析構(gòu)函數(shù)中解鎖,也就是說(shuō),只要?jiǎng)?chuàng)建一個(gè)lock_guard的對(duì)象,就相當(dāng)于lock()了,而該對(duì)象析構(gòu)時(shí),就自動(dòng)調(diào)用unlock()了。
就以上述程序?yàn)槔苯痈膶?xiě)為:
void WriteList() { while(i<10000) { lock_guard<mutex> guard(m); //創(chuàng)建lock_guard的類(lèi)對(duì)象guard,用互斥量m來(lái)構(gòu)造 //m.lock(); if (i >= 10000) { //m.unlock(); //由于有了guard,這里就無(wú)需unlock()了 break; } mylist.push_back(i++); //m.unlock(); } return; }
這里主要有兩個(gè)需要注意的地方:第一、原先的lock()和unlock()都不用了;第二、if中的break前面也不用再調(diào)用unlock()了。這都是因?yàn)閷?duì)象guard在lock_guard一句處構(gòu)造出來(lái),同時(shí)就調(diào)用了lock(),當(dāng)退出while時(shí),guard析構(gòu),析構(gòu)時(shí)就調(diào)用了unlock()。(局部對(duì)象的生命周期就是創(chuàng)建該對(duì)象時(shí)離其最近的大括號(hào)的范圍{})
3.死鎖
3.1 死鎖的含義
死鎖是什么意思呢?舉個(gè)例子,我和你手里都拽著對(duì)方家門(mén)的鑰匙,我說(shuō):“你不把我的鎖還來(lái),我就不把你的鎖給你!”,你一聽(tīng)不樂(lè)意了,也說(shuō):“你不把我的鎖還來(lái),我也不把你的鎖給你!”就這樣,我們兩個(gè)人互相拿著對(duì)方的鎖又等著對(duì)方先把鎖拿來(lái),然后就只能一直等著等著等著......最終誰(shuí)也拿不到自己的鎖,這就是死鎖。
顯然,死鎖是發(fā)生在至少兩個(gè)鎖之間的,也就是指由于兩個(gè)或者多個(gè)線程互相持有對(duì)方所需要的資源,導(dǎo)致這些線程處于等待狀態(tài),無(wú)法前往執(zhí)行,當(dāng)線程互相持有對(duì)方所需要的資源時(shí),會(huì)互相等待對(duì)方釋放資源,如果線程都不主動(dòng)釋放所占有的資源,將產(chǎn)生死鎖。
3.2 死鎖的例子
mutex m0,m1; int i = 0; void fun0() { while (i < 100) { lock_guard<mutex> g0(m0); //線程0加鎖0 lock_guard<mutex> g1(m1); //線程0加鎖1 cout << "thread 0 running..." << endl; } return; } void fun1() { while (i < 100) { lock_guard<mutex> g1(m1); //線程1加鎖1 lock_guard<mutex> g0(m0); //線程1加鎖0 cout << "thread 1 running... "<< i << endl; } return; } int main() { thread p0(fun0); thread p1(fun1); p0.join(); p1.join(); return 0; }
我們來(lái)看下運(yùn)行結(jié)果:
這就出現(xiàn)了死鎖。產(chǎn)生的原因就是因?yàn)樵诰€程0中,先加鎖0,再加鎖1;在線程1中,先加鎖1,再加鎖0;如果兩個(gè)線程之一能夠完整執(zhí)行的話(huà),那自然是沒(méi)有問(wèn)題的,但是如果某個(gè)時(shí)刻,線程0中剛加鎖0,就上下文切換到線程1,此時(shí)線程1就加鎖1,然后此時(shí)兩個(gè)線程都想向下執(zhí)行的話(huà),線程1就必須等待線程0解鎖0,線程0就必須等待線程1解鎖1,就這樣兩個(gè)線程都一直阻塞著,形成了死鎖。
3.3 死鎖的解決方法
①按順序加鎖
以上述例程來(lái)說(shuō),就是線程0和線程1的加鎖順序保持一致,如下所示:
mutex m0,m1; int i = 0; void fun0() { while (i < 100) { lock_guard<mutex> g0(m0); //線程0加鎖0 lock_guard<mutex> g1(m1); //線程0加鎖1 cout << "thread 0 running..." << endl; } return; } void fun1() { while (i < 100) { lock_guard<mutex> g0(m0); //線程1加鎖0 lock_guard<mutex> g1(m1); //線程1加鎖1 cout << "thread 1 running... "<< i << endl; } return; } int main() { thread p0(fun0); thread p1(fun1); p0.join(); p1.join(); return 0; }
在這種情況下,兩個(gè)線程一旦一個(gè)加了鎖,那么另一個(gè)就必定阻塞,這樣,就不會(huì)出現(xiàn)兩邊加鎖兩邊阻塞的情況,從而避免死鎖。
②同時(shí)上鎖
同時(shí)上鎖需要用到lock()函數(shù),如下所述:
mutex m0,m1; int i = 0; void fun0() { while (i < 100) { lock(m0,m1); lock_guard<mutex> g0(m0, adopt_lock); lock_guard<mutex> g1(m1, adopt_lock); cout << "thread 0 running..." << endl; } return; } void fun1() { while (i < 100) { lock(m0,m1); lock_guard<mutex> g0(m0, adopt_lock); lock_guard<mutex> g1(m1, adopt_lock); cout << "thread 1 running... "<< i << endl; } return; } int main() { thread p0(fun0); thread p1(fun1); p0.join(); p1.join(); return 0; }
注意到這里的lock_guard中多了第二個(gè)參數(shù)adopt_lock,這個(gè)參數(shù)表示在調(diào)用lock_guard時(shí),已經(jīng)加鎖了,防止lock_guard在對(duì)象生成時(shí)構(gòu)造函數(shù)再次lock()。?
以上就是C++多線程之互斥鎖與死鎖的詳細(xì)內(nèi)容,更多關(guān)于C++ 多線程 互斥鎖 死鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++類(lèi)成員構(gòu)造函數(shù)和析構(gòu)函數(shù)順序示例詳細(xì)講解
這篇文章主要介紹了C++類(lèi)成員構(gòu)造和析構(gòu)順序示例,看了這個(gè)例子大家就可以明白c++構(gòu)造析構(gòu)的奧秘2013-11-11面試常見(jiàn)問(wèn)題之C語(yǔ)言與C++的區(qū)別問(wèn)題
在C中,用static修飾的變量或函數(shù),主要用來(lái)說(shuō)明這個(gè)變量或函數(shù)只能在本文件代碼塊中訪問(wèn),而文件外部的代碼無(wú)權(quán)訪問(wèn),今天重點(diǎn)給大家介紹面試中常見(jiàn)的C語(yǔ)言與C++區(qū)別的問(wèn)題,感興趣的朋友跟隨小編一起看看吧2021-05-05C++編程中私有和保護(hù)以及公有的類(lèi)成員訪問(wèn)控制
這篇文章主要介紹了C++編程中私有和保護(hù)以及公有的類(lèi)成員訪問(wèn)控制,即private和protected以及public關(guān)鍵字的相關(guān)作用和用法,需要的朋友可以參考下2016-01-01C++實(shí)現(xiàn)學(xué)校運(yùn)動(dòng)會(huì)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)學(xué)校運(yùn)動(dòng)會(huì)管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10C++結(jié)構(gòu)體字節(jié)對(duì)齊示例
這篇文章主要為大家介紹了C++結(jié)構(gòu)體字節(jié)對(duì)齊示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06