當(dāng)前位置:首頁(yè) > 嵌入式培訓(xùn) > 嵌入式學(xué)習(xí) > 講師博文 > Linux多線程與同步
Linux多線程與同步
時(shí)間:2018-09-29 來(lái)源:未知
典型的UNIX系統(tǒng)都支持一個(gè)進(jìn)程創(chuàng)建多個(gè)線程(thread)。在Linux進(jìn)程基礎(chǔ)中提到,Linux以進(jìn)程為單位組織操作,Linux中的線程也都基于進(jìn)程。盡管實(shí)現(xiàn)方式有異于其它的UNIX系統(tǒng),但Linux的多線程在邏輯和使用上與真正的多線程并沒(méi)有差別。
在Linux從程序到進(jìn)程中提到的stack的功能和用途。由于stack中只有下方的stack frame可以被讀取,所以我們只能有該frame對(duì)應(yīng)的單一函數(shù)處于激活狀態(tài)。為了實(shí)現(xiàn)多線程,我們必須繞開(kāi)stack帶給我們的限制。為此,當(dāng)我們創(chuàng)建一個(gè)新的線程的時(shí)候,我們?yōu)檫@個(gè)線程創(chuàng)建一個(gè)新的stack。當(dāng)該stack執(zhí)行到全部彈出的時(shí)候,該線程完成任務(wù)并結(jié)束。所以,多線程的進(jìn)程在內(nèi)存中會(huì)有多個(gè)stack,相互之間以一定的空白區(qū)域隔開(kāi),以備stack的增長(zhǎng)。每個(gè)線程隨時(shí)可以使用自己stack中下方的stack frame中的參數(shù)和變量,并與其它線程共享內(nèi)存中的Text,heap和global data區(qū)域。在上面的例子中,我們將在進(jìn)程空間中有三個(gè)stack。
(要注意的是,對(duì)于多線程來(lái)說(shuō),由于同一個(gè)進(jìn)程空間中存在多個(gè)stack,任何一個(gè)空白區(qū)域被填滿(mǎn)都會(huì)導(dǎo)致stack overflow的問(wèn)題。)
多線程相當(dāng)于一個(gè)并發(fā)(concunrrency)系統(tǒng)。并發(fā)系統(tǒng)一般同時(shí)執(zhí)行多個(gè)任務(wù)。如果多個(gè)任務(wù)可以共享資源,特別是同時(shí)寫(xiě)入某個(gè)變量的時(shí)候,就需要解決同步的問(wèn)題。比如說(shuō),我們有一個(gè)多線程火車(chē)售票系統(tǒng),用全局變量i存儲(chǔ)剩余的票數(shù)。多個(gè)線程不斷地賣(mài)票(i = i - 1),直到剩余票數(shù)為0。所以每個(gè)都需要執(zhí)行如下操作:
/*mu is a global mutex*/
while (1) { /*infinite loop*/
if (i != 0) i = i -1
else {
printf("no more tickets");
exit();
}
}
如果只有一個(gè)線程執(zhí)行上面的程序的時(shí)候(相當(dāng)于一個(gè)窗口售票),則沒(méi)有問(wèn)題。但如果多個(gè)線程都執(zhí)行上面的程序(相當(dāng)于多個(gè)窗口售票), 我們就會(huì)出現(xiàn)問(wèn)題。我們會(huì)看到,其根本原因在于同時(shí)發(fā)生的各個(gè)線程都可以對(duì)i讀取和寫(xiě)入。
我們這里的if結(jié)構(gòu)會(huì)給CPU兩個(gè)指令, 一個(gè)是判斷是否有剩余的票(i != 0), 一個(gè)是賣(mài)票 (i = i -1)。某個(gè)線程會(huì)先判斷是否有票(比如說(shuō)此時(shí)i為1),但兩個(gè)指令之間存在一個(gè)時(shí)間窗口,其它線程可能在此時(shí)間窗口內(nèi)執(zhí)行賣(mài)票操作(i = i -1),導(dǎo)致該線程賣(mài)票的條件不再成立。但該線程由于已經(jīng)執(zhí)行過(guò)了判斷指令,所以無(wú)從知道i發(fā)生了變化,所以繼續(xù)執(zhí)行賣(mài)票指令,以至于賣(mài)出不存在的票 (i成為負(fù)數(shù))。對(duì)于一個(gè)真實(shí)的售票系統(tǒng)來(lái)說(shuō),這將成為一個(gè)嚴(yán)重的錯(cuò)誤 (售出了過(guò)多的票,火車(chē)爆滿(mǎn))。
在并發(fā)情況下,指令執(zhí)行的先后順序由內(nèi)核決定。同一個(gè)線程內(nèi)部,指令按照先后順序執(zhí)行,但不同線程之間的指令很難說(shuō)清除哪一個(gè)會(huì)先執(zhí)行。如果運(yùn)行的結(jié)果依賴(lài)于不同線程執(zhí)行的先后的話,那么就會(huì)造成競(jìng)爭(zhēng)條件(race condition),在這樣的狀況下,計(jì)算機(jī)的結(jié)果很難預(yù)知。我們應(yīng)該盡量避免競(jìng)爭(zhēng)條件的形成。常見(jiàn)的解決競(jìng)爭(zhēng)條件的方法是將原先分離的兩個(gè)指令構(gòu)成不可分隔的一個(gè)原子操作(atomic operation),而其它任務(wù)不能插入到原子操作中。
3. 多進(jìn)程同步(synchronization)
對(duì)于多線程程序來(lái)說(shuō),同步是指在一定的時(shí)間內(nèi)只允許某一個(gè)線程訪問(wèn)某個(gè)資源 。而在此時(shí)間內(nèi),不允許其它的線程訪問(wèn)該資源。我們可以通過(guò)互斥鎖(mutex),條件變量(condition variable)和讀寫(xiě)鎖(reader-writer lock)來(lái)同步資源。
1) mutex
mutex是一個(gè)特殊的變量,它有鎖上(lock)和打開(kāi)(unlock)兩個(gè)狀態(tài)。mutex一般被設(shè)置成全局變量。打開(kāi)的mutex可以由某個(gè)線程獲得。一旦獲得,這個(gè)mutex會(huì)鎖上,此后只有該線程有權(quán)打開(kāi)。其它想要獲得mutex的線程,會(huì)等待直到mutex再次打開(kāi)的時(shí)候。我們可以將mutex想像成為一個(gè)只能容納一個(gè)人的洗手間,當(dāng)某個(gè)人進(jìn)入洗手間的時(shí)候,可以從里面將洗手間鎖上。其它人只能在mutex外面等待那個(gè)人出來(lái),才能進(jìn)去。在外面等候的人并沒(méi)有排隊(duì),誰(shuí)先看到洗手間空了,就可以首先沖進(jìn)去。
上面的問(wèn)題很容易使用mutex的問(wèn)題解決,每個(gè)進(jìn)程的程序可以改為:
/*mu is a global mutex*/
while (1) { /*infinite loop*/
mutex_lock(mu); /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/
if (i != 0) i = i - 1;
else {
printf("no more tickets");
exit();
}
mutex_unlock(mu); /*release mutex, make it unblocked*/
}
第一個(gè)執(zhí)行mutex_lock()的線程會(huì)先獲得mu。其它想要獲得mu的線程必須等待,直到第一個(gè)線程執(zhí)行到mutex_unlock()釋放mu,才可以獲得mu,并繼續(xù)執(zhí)行線程。所以線程在mutex_lock()和mutex_unlock()之間的操作時(shí),不會(huì)被其它線程影響,就構(gòu)成了一個(gè)原子操作。
需要注意的時(shí)候,如果存在某個(gè)進(jìn)程依然使用原先的程序 (即不嘗試獲得mu,而直接修改i),mutex不能阻止該程序修改i,mutex就失去了保護(hù)資源的意義。所以,mutex機(jī)制需要程序員自己來(lái)寫(xiě)出完善的程序來(lái)實(shí)現(xiàn)mutex的功能。我們下面講的其它機(jī)制也是如此。
2) condition variable
condition variable是另一種常用的變量。它也常常被保存為全局變量,并和mutex合作。
假設(shè)我們有這樣一個(gè)狀況: 我們有100個(gè)工人,每人負(fù)責(zé)裝修一個(gè)房間。當(dāng)有10個(gè)房間裝修完成的時(shí)候,我們就通知相應(yīng)的十個(gè)工人一起去喝啤酒。我們?nèi)绾螌?shí)現(xiàn)呢?我們可以讓工人在裝修好房間之后,去檢查已經(jīng)裝修好的房間數(shù)。但多線程條件下,會(huì)有競(jìng)爭(zhēng)條件的危險(xiǎn)(其他工人會(huì)在該工人裝修好房子和檢查之間完成工作)。
/*mu: global mutex, cond: global codition variable, num: global int*/
mutex_lock(mu)
num = num + 1; /*worker build the room*/
if (num <= 10) { /*worker is within the first 10 to finish*/
cond_wait(mu, cond); /*wait*/
printf("drink beer");
}
else if (num = 11) { /*workder is the 11th to finish*/
cond_broadcast(mu, cond); /*inform the other 9 to wake up*/
}
mutex_unlock(mu);
通常, condition variable除了要和mutex配合之外,還需要和另一個(gè)全局變量配合(這里的num, 也就是裝修好的房間數(shù))。這個(gè)全局變量用來(lái)構(gòu)成各個(gè)條件。
我們讓工人在裝修好房間(num = num + 1)之后,去檢查已經(jīng)裝修好的房間數(shù)( num < 10 )。由于mu被鎖上,所以不會(huì)有其他工人在此期間裝修房間(改變num的值)。如果該工人是前十個(gè)完成的人,那么我們就調(diào)用cond_wait()函數(shù)。
cond_wait()做兩件事情,一個(gè)是釋放mu,從而讓別的工人可以建房。另一個(gè)是等待,直到cond的通知。這樣的話,符合條件的線程就開(kāi)始等待。當(dāng)有通知(第十個(gè)房間已經(jīng)修建好)到達(dá)的時(shí)候,condwait()會(huì)再次鎖上mu,并恢復(fù)線程的運(yùn)行,我們會(huì)執(zhí)行下一句prinft("drink beer") (我們以此來(lái)代表喝啤酒)。此后直到mutex_unlock()就構(gòu)成了另一個(gè)mutex結(jié)構(gòu)。
那么如何讓前面十個(gè)調(diào)用cond_wait()的線程得到通知呢?我們注意到if還有另一種可能,也就是修建好第11個(gè)房間的人負(fù)責(zé)調(diào)用cond_broadcast()。它會(huì)給所有調(diào)用cond_wait()的進(jìn)程放送通知,以便讓那些進(jìn)程恢復(fù)運(yùn)行。
Condition variable特別適用于多個(gè)線程等待某個(gè)條件的發(fā)生。如果不使用Condition variable,那么每個(gè)進(jìn)程就需要不斷嘗試獲得mutex并檢查條件是否發(fā)生,這樣大大浪費(fèi)了系統(tǒng)的資源。
3) reader-writer lock
Reader-writer lock與mutex非常相似。r、RW lock有三種狀態(tài): 共享讀取鎖(shared-read), 互斥寫(xiě)入鎖(exclusive-write lock), 打開(kāi)(unlock)。后兩種狀態(tài)與之前的mutex兩種狀態(tài)完全相同。
一個(gè)unlock的RW lock可以被某個(gè)進(jìn)程獲取R鎖或者W鎖。
如果被一個(gè)進(jìn)程獲得R鎖,RW lock可以被其它進(jìn)程繼續(xù)獲得R鎖,而不必等待該進(jìn)程釋放R鎖。但是,如果此時(shí)有其它進(jìn)程想要獲得W鎖,它必須等到所有持有共享讀取鎖的進(jìn)程釋放掉各自的R鎖。
如果一個(gè)鎖被一個(gè)進(jìn)程獲得W鎖,那么其它進(jìn)程,無(wú)論是想要獲取R鎖還是W鎖,都必須等待該進(jìn)程釋放W鎖。
這樣,多個(gè)進(jìn)程就可以同時(shí)讀取共享資源。而具有危險(xiǎn)性的寫(xiě)入操作則得到了互斥鎖的保護(hù)。
我們需要同步并發(fā)系統(tǒng),這為程序員編程帶來(lái)了難度。但是多線程系統(tǒng)可以很好的解決許多IO瓶頸的問(wèn)題。比如我們監(jiān)聽(tīng)網(wǎng)絡(luò)端口。如果我們只有一個(gè)線程,那么我們必須監(jiān)聽(tīng),接收請(qǐng)求,處理,回復(fù),再監(jiān)聽(tīng)。如果我們使用多線程系統(tǒng),則可以讓多個(gè)線程監(jiān)聽(tīng)。當(dāng)我們的某個(gè)線程進(jìn)行處理的時(shí)候,我們還可以有其他的線程繼續(xù)監(jiān)聽(tīng),這樣,就大大提高了系統(tǒng)的利用率。在數(shù)據(jù)越來(lái)越大,服務(wù)器讀寫(xiě)操作越來(lái)越多的今天,這具有相當(dāng)?shù)囊饬x。多線程還可以更有效地利用多CPU的環(huán)境。
(就像做飯一樣,不斷切換去處理不同的菜。)
總結(jié):
multiple threads, multiple stacks
race condition
mutex, condition variable, RW lock
華清遠(yuǎn)見(jiàn)90+項(xiàng)目獲批!教育部2021最新協(xié)同育人項(xiàng)目名
華清遠(yuǎn)見(jiàn)榮獲2021騰訊教育“年度口碑影響力職業(yè)教育品
華清遠(yuǎn)見(jiàn)受邀參加2021年武漢民辦高校信息學(xué)科合作聯(lián)盟
溫暖同行共創(chuàng)佳績(jī) 2019華清遠(yuǎn)見(jiàn)北京總部年會(huì)大曝光
助力高校AI人工智能學(xué)科建設(shè) 華清遠(yuǎn)見(jiàn)人工智能師資班
華清遠(yuǎn)見(jiàn)受邀參加四川省物聯(lián)網(wǎng)年會(huì),榮獲優(yōu)秀企業(yè)專(zhuān)家
