C++多線程之互斥鎖與死鎖_第1頁
C++多線程之互斥鎖與死鎖_第2頁
C++多線程之互斥鎖與死鎖_第3頁
C++多線程之互斥鎖與死鎖_第4頁
C++多線程之互斥鎖與死鎖_第5頁
已閱讀5頁,還剩8頁未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

第C++多線程之互斥鎖與死鎖目錄1.前言2.互斥鎖2.1互斥鎖的特點(diǎn)2.2互斥鎖的使用2.3std::lock_guard3.死鎖3.1死鎖的含義3.2死鎖的例子3.3死鎖的解決方法

1.前言

比如說我們現(xiàn)在以一個(gè)list容器來模仿一個(gè)消息隊(duì)列,當(dāng)消息來臨時(shí)插入list的尾部,當(dāng)讀取消息時(shí)就把頭部的消息讀出來并且刪除這條消息。在代碼中就以兩個(gè)線程分別實(shí)現(xiàn)消息寫入和消息讀取的功能,如下:

classmsgList

private:

listintmylist;//用list模仿一個(gè)消息隊(duì)列

public:

voidWriteList()//向消息隊(duì)列中寫入消息(以i作為消息)

for(inti=0;i100000;i++)

cout"Write:"iendl;

mylist.push_back(i);

return;

voidReadList()//從消息隊(duì)列中讀取并取出消息

for(inti=0;i100000;i++)

if(!mylist.empty())

cout"Read:"mylist.front()endl;

mylist.pop_front();

else

cout"MessageListisempty!"endl;

intmain()

msgListmlist;

threadpread(msgList::ReadList,mlist);//讀線程

threadpwrite(msgList::WriteList,mlist);//寫線程

//等待線程結(jié)束

pread.join();

pwrite.join();

return0;

}

這段程序在運(yùn)行過程中,大部分時(shí)間是正常的,但是也會(huì)出現(xiàn)如下不穩(wěn)定的情況:

為什么會(huì)出現(xiàn)這種情況呢?

這是因?yàn)橄㈥?duì)列對(duì)于讀線程和寫線程來說是共享的,這時(shí)就會(huì)出現(xiàn)兩種特殊的情況:讀線程的讀取操作還沒有結(jié)束,線程上下文就切換到了寫線程中;或者寫線程的寫入操作還沒有結(jié)束,線程上下文切換就到了讀線程中,這兩種情況都反映了讀寫沖突,從而出現(xiàn)了以上錯(cuò)誤。

要想解決這個(gè)問題,最顯然最直接的方法就是將讀寫操作分離開來,讀的時(shí)候不允許寫,寫的時(shí)候不允許讀,這樣,才能實(shí)現(xiàn)線程安全的讀和寫。說形象一點(diǎn),就是在進(jìn)行讀操作時(shí),就對(duì)共享資源進(jìn)行加鎖,禁止其他線程訪問,其他線程要訪問就得等到讀線程解鎖才行,就像上廁所一樣,一次只能上一個(gè)人,其他人必須得等他上完了再上。這樣,就有了互斥鎖的概念。

2.互斥鎖

在多任務(wù)操作系統(tǒng)中,同時(shí)運(yùn)行的多個(gè)任務(wù)可能都需要使用同一種資源。比如說,同一個(gè)文件,可能一個(gè)線程會(huì)對(duì)其進(jìn)行寫操作,而另一個(gè)線程需要對(duì)這個(gè)文件進(jìn)行讀操作,可想而知,如果寫線程還沒有寫結(jié)束,而此時(shí)讀線程開始了,或者讀線程還沒有讀結(jié)束而寫線程開始了,那么最終的結(jié)果顯然會(huì)是混亂的。為了保護(hù)共享資源,在線程里也有這么一把鎖——互斥鎖(mutex),互斥鎖是一種簡單的加鎖的方法來控制對(duì)共享資源的訪問,互斥鎖只有兩種狀態(tài),即上鎖(lock)和解鎖(unlock)。

2.1互斥鎖的特點(diǎn)

1.原子性:把一個(gè)互斥量鎖定為一個(gè)原子操作,這意味著如果一個(gè)線程鎖定了一個(gè)互斥量,沒有其他線程在同一時(shí)間可以成功鎖定這個(gè)互斥量;

2.唯一性:如果一個(gè)線程鎖定了一個(gè)互斥量,在它解除鎖定之前,沒有其他線程可以鎖定這個(gè)互斥量;

3.非繁忙等待:如果一個(gè)線程已經(jīng)鎖定了一個(gè)互斥量,第二個(gè)線程又試圖去鎖定這個(gè)互斥量,則第二個(gè)線程將被掛起(不占用任何cpu資源),直到第一個(gè)線程解除對(duì)這個(gè)互斥量的鎖定為止,第二個(gè)線程則被喚醒并繼續(xù)執(zhí)行,同時(shí)鎖定這個(gè)互斥量。

2.2互斥鎖的使用

根據(jù)前面我們可以知道,互斥鎖主要就是用來保護(hù)共享資源的,在C++11中,互斥鎖封裝在mutex類中,通過調(diào)用類成員函數(shù)lock()和unlock()來實(shí)現(xiàn)加鎖和解鎖。值得注意的是,加鎖和解鎖,必須成對(duì)使用,這也是比較好理解的。除此之外,互斥量的使用時(shí)機(jī),就以開篇程序?yàn)槔覀円Wo(hù)的共享資源當(dāng)然就是消息隊(duì)列l(wèi)ist了,那么互斥鎖應(yīng)該加在哪里呢?

可能想的比較簡單一點(diǎn):就直接把鎖加在函數(shù)最前面不就好了么?如下所示:

classmsgList

private:

listintmylist;//用list模仿一個(gè)消息隊(duì)列

mutexmtx;//創(chuàng)建互斥鎖對(duì)象

public:

voidWriteList()//向消息隊(duì)列中寫入消息(以i作為消息)

mtx.lock();

for(inti=0;i100000;i++)

cout"Write:"iendl;

mylist.push_back(i);

mtx.unlock();

return;

//.......

};

不過如果這樣加鎖的話,要等寫線程完全執(zhí)行結(jié)束才能開始讀線程,讀寫線程變成了串行執(zhí)行,這就違背了線程并發(fā)性的特點(diǎn)了。正確的加鎖方式應(yīng)當(dāng)是在執(zhí)行寫操作的具體部分加鎖,如下所示:

classmsgList

private:

listintmylist;//用list模仿一個(gè)消息隊(duì)列

mutexmtx;//創(chuàng)建互斥鎖對(duì)象

public:

voidWriteList()//向消息隊(duì)列中寫入消息(以i作為消息)

for(inti=0;i100000;i++)

mtx.lock();

cout"Write:"iendl;

mylist.push_back(i);

mtx.unlock();

return;

//.......

};

這樣,才能真正的實(shí)現(xiàn)讀寫互不干擾。

下面再舉一個(gè)更為直觀的例子,創(chuàng)建兩個(gè)線程同時(shí)對(duì)list進(jìn)行寫操作:

classmsgList

private:

listintmylist;

mutexm;

inti=0;

public:

voidWriteList()

while(i1000)

mylist.push_back(i++);

return;

voidshowList()

for(autop=mylist.begin();p!=mylist.end();p++)

cout(*p)"";

coutendl;

cout"sizeoflist:"mylist.size()endl;

return;

intmain()

msgListmlist;

threadpwrite0(msgList::WriteList,mlist);

threadpwrite1(msgList::WriteList,mlist);

pwrite0.join();

pwrite1.join();

cout"threadsend!"endl;

mlist.showList();//子線程結(jié)束后主線程打印list

return0;

}

這里用兩個(gè)線程來寫list,并且最終在主線程中調(diào)用了showList()來輸出list的size和所有元素,我們先來看下輸出情況:

根據(jù)結(jié)果可以看到,這里有很多問題:實(shí)際輸出的元素個(gè)數(shù)和size不符,輸出的元素也并不是連續(xù)的,這都是多個(gè)線程同時(shí)更新list所造成的情況。這種情況下,運(yùn)行結(jié)果是無法預(yù)料的,每次都可能不一樣。這就是線程不安全所引發(fā)的問題,我們加上鎖再來看看:

classmsgList

private:

listintmylist;

mutexm;

inti=0;

public:

voidWriteList()

while(i1000)

m.lock();//加鎖

mylist.push_back(i++);

m.unlock();//解鎖

return;

//......

};

這樣加鎖就正確了嗎?我們?cè)俣噙\(yùn)行幾次看看:

數(shù)字都是連續(xù)的,但是個(gè)數(shù)卻多了一個(gè)(出現(xiàn)的幾率還是比較小),這又是什么原因造成的呢?還是兩個(gè)線程的問題,假設(shè)要插入1000個(gè)數(shù),循環(huán)條件就是while(i1000),當(dāng)i=999的時(shí)候兩個(gè)寫線程都可以進(jìn)入while循環(huán),此時(shí)如果pwrite0線程拿到了lock(),那么pwrite1線程就只能一直等待,pwrite0線程繼續(xù)往下執(zhí)行,使得i變成了1000,此時(shí),對(duì)于pwrite0線程來說,它就必須退出循環(huán)了。而此時(shí)的pwrite1在哪里呢?還等在lock()的地方,pwrite0線程unlock()后,pwrite1成功lock(),此時(shí)i=1000,但是pwrite1卻還沒有執(zhí)行完此次循環(huán),因此向list中插入1000,此時(shí)退出的i的值為1001,這也就造成了實(shí)際輸出為1001個(gè)數(shù)的情況。

為了避免這個(gè)問題,一個(gè)簡單的辦法就是在lock()之后再加上一個(gè)判斷,判斷i是否依舊滿足while的條件,如下:

voidWriteList()

while(i10000)

m.lock();

if(i=10000)

m.unlock();//退出之前必須先解鎖

break;

mylist.push_back(i++);

m.unlock();

return;

}

為什么這里要在break前面加一個(gè)unlock()呢?原因就在于:如果break前面沒有unlock(),一旦i符合了if的條件,就直接break了,此時(shí)就沒法unlock(),程序就會(huì)報(bào)錯(cuò):

可以發(fā)現(xiàn),這種錯(cuò)誤是比較難發(fā)現(xiàn)的,特別是像這樣程序中出現(xiàn)了分支的情況,很容易就使得程序?qū)嶋H運(yùn)行時(shí)lock()了卻沒有unclock()。為了解決這一問題,就有了std::lock_guard。

2.3std::lock_guard

簡單來理解的話,lock_guard就是一個(gè)類,它會(huì)在其構(gòu)造函數(shù)中加鎖,而在析構(gòu)函數(shù)中解鎖,也就是說,只要?jiǎng)?chuàng)建一個(gè)lock_guard的對(duì)象,就相當(dāng)于lock()了,而該對(duì)象析構(gòu)時(shí),就自動(dòng)調(diào)用unlock()了。

就以上述程序?yàn)槔苯痈膶憺椋?/p>

voidWriteList()

while(i10000)

lock_guardmutexguard(m);//創(chuàng)建lock_guard的類對(duì)象guard,用互斥量m來構(gòu)造

//m.lock();

if(i=10000)

//m.unlock();//由于有了guard,這里就無需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)造出來,同時(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ì)方家門的鑰匙,我說:“你不把我的鎖還來,我就不把你的鎖給你!”,你一聽不樂意了,也說:“你不把我的鎖還來,我也不把你的鎖給你!”就這樣,我們兩個(gè)人互相拿著對(duì)方的鎖又等著對(duì)方先把鎖拿來,然后就只能一直等著等著等著......最終誰也拿不到自己的鎖,這就是死鎖。

顯然,死鎖是發(fā)生在至少兩個(gè)鎖之間的,也就是指由于兩個(gè)或者多個(gè)線程互相持有對(duì)方所需要的資源,導(dǎo)致這些線程處于等待狀態(tài),無法前往執(zhí)行,當(dāng)線程互相持有對(duì)方所需要的資源時(shí),會(huì)互相等待對(duì)方釋放資源,如果線程都不主動(dòng)釋放所占有的資源,將產(chǎn)生死鎖。

3.2死鎖的例子

mutexm0,m1;

inti=0;

voidfun0()

while(i100)

lock_guardmutexg0(m0);//線程0加鎖0

lock_guardmutexg1(m1);//線程0加鎖1

cout"thread0running..."endl;

return;

voidfun1()

while(i100)

lock_guardmutexg1(m1);//線程1加鎖1

lock_guardmutexg0(m0);//線程1加鎖0

cout"thread1running..."iendl;

return;

intmain()

threadp0(fun0);

threadp1(fun1);

p0.join();

p1.join();

return0;

}

我們來看下運(yùn)行結(jié)果:

這就出現(xiàn)了死鎖。產(chǎn)生的原因就是因?yàn)樵诰€程0中,先加鎖0,再加鎖1;在線程1中,先加鎖1,再加鎖0;如果兩個(gè)線程之一能夠完整執(zhí)行的話,那自然是沒有問題的,但是如果某個(gè)時(shí)刻,線程0中剛加鎖0,就上下文切換到線程1,此時(shí)線程1就加鎖1,然后此時(shí)兩個(gè)線程都想向下執(zhí)行的話,線程1就必須等待線程0解鎖0,線程0就必須等待線程1解鎖1,就這樣兩個(gè)線程都一直阻塞著,形成了死鎖。

3.3死鎖的解決方法

①按順序加鎖

以上述例程來說,就是線程0和線程1的加鎖順序保持一致,如下所示:

mutexm0,m1;

inti=0;

voidfun0()

while(i100)

lock_guardmutexg0(m0);//線程0加鎖0

lock_guardmutexg1(m1);//線程0加鎖1

cout"thread0running..."endl;

return;

voidfun1()

while(i100)

lock_guardmutexg0(m0);//線程1加鎖0

lock_guardmutexg1(m1);//線程1加鎖1

cout"thread1running..."iendl;

return;

intmain()

threadp0(fun0);

threadp1(fun1);

p0.join();

p1.join();

return0;

}

在這種情況下,兩個(gè)線

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論