典型的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的多線程在邏輯和使用上與真正的多線程并沒有差別。
?
多線程
我們先來看一下什么是多線程。在Linux從程序到進(jìn)程中,我們看到了一個(gè)程序在內(nèi)存中的表示。這個(gè)程序的整個(gè)運(yùn)行過程中,只有一個(gè)控制權(quán)的存在。當(dāng)函數(shù)被調(diào)用的時(shí)候,該函數(shù)獲得控制權(quán),成為激活(active)函數(shù),然后運(yùn)行該函數(shù)中的指令。與此同時(shí),其它的函數(shù)處于離場(chǎng)狀態(tài),并不運(yùn)行。如下圖所示:
Linux從程序到進(jìn)程
?
我們看到,各個(gè)方塊之間由箭頭連接。各個(gè)函數(shù)就像是連在一根線上一樣,計(jì)算機(jī)像一條流水線一樣執(zhí)行各個(gè)函數(shù)中定義的操作。這樣的一個(gè)程序叫做單線程程序。
多線程就是允許一個(gè)進(jìn)程內(nèi)存在多個(gè)控制權(quán),以便讓多個(gè)函數(shù)同時(shí)處于激活狀態(tài),從而讓多個(gè)函數(shù)的操作同時(shí)運(yùn)行。即使是單CPU的計(jì)算機(jī),也可以通過不停地在不同線程的指令間切換,從而造成多線程同時(shí)運(yùn)行的效果。如下圖所示,就是一個(gè)多線程的流程:
main()到func3()再到main()構(gòu)成一個(gè)線程,此外func1()和func2()構(gòu)成另外兩個(gè)線程。操作系統(tǒng)一般都有一些系統(tǒng)調(diào)用來讓你將一個(gè)函數(shù)運(yùn)行成為一個(gè)新的線程。
?
回憶我們?cè)贚inux從程序到進(jìn)程中提到的棧的功能和用途。一個(gè)棧,只有最下方的幀可被讀寫。相應(yīng)的,也只有該幀對(duì)應(yīng)的那個(gè)函數(shù)被激活,處于工作狀態(tài)。為了實(shí)現(xiàn)多線程,我們必須繞開棧的限制。為此,創(chuàng)建一個(gè)新的線程時(shí),我們?yōu)檫@個(gè)線程建一個(gè)新的棧。每個(gè)棧對(duì)應(yīng)一個(gè)線程。當(dāng)某個(gè)棧執(zhí)行到全部彈出時(shí),對(duì)應(yīng)線程完成任務(wù),并收工。所以,多線程的進(jìn)程在內(nèi)存中有多個(gè)棧。多個(gè)棧之間以一定的空白區(qū)域隔開,以備棧的增長(zhǎng)。每個(gè)線程可調(diào)用自己棧最下方的幀中的參數(shù)和變量,并與其它線程共享內(nèi)存中的Text,heap和global data區(qū)域。對(duì)應(yīng)上面的例子,我們的進(jìn)程空間中需要有3個(gè)棧。
(要注意的是,對(duì)于多線程來說,由于同一個(gè)進(jìn)程空間中存在多個(gè)棧,任何一個(gè)空白區(qū)域被填滿都會(huì)導(dǎo)致stack overflow的問題。)
?
并發(fā)
多線程相當(dāng)于一個(gè)并發(fā)(concunrrency)系統(tǒng)。并發(fā)系統(tǒng)一般同時(shí)執(zhí)行多個(gè)任務(wù)。如果多個(gè)任務(wù)可以共享資源,特別是同時(shí)寫入某個(gè)變量的時(shí)候,就需要解決同步的問題。比如說,我們有一個(gè)多線程火車售票系統(tǒng),用全局變量i存儲(chǔ)剩余的票數(shù)。多個(gè)線程不斷地賣票(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è)窗口售票),則沒有問題。但如果多個(gè)線程都執(zhí)行上面的程序(相當(dāng)于多個(gè)窗口售票), 我們就會(huì)出現(xiàn)問題。我們會(huì)看到,其根本原因在于同時(shí)發(fā)生的各個(gè)線程都可以對(duì)i讀取和寫入。
我們這里的if結(jié)構(gòu)會(huì)給CPU兩個(gè)指令, 一個(gè)是判斷是否有剩余的票(i != 0), 一個(gè)是賣票 (i = i -1)。某個(gè)線程會(huì)先判斷是否有票(比如說此時(shí)i為1),但兩個(gè)指令之間存在一個(gè)時(shí)間窗口,其它線程可能在此時(shí)間窗口內(nèi)執(zhí)行賣票操作(i = i -1),導(dǎo)致該線程賣票的條件不再成立。但該線程由于已經(jīng)執(zhí)行過了判斷指令,所以無從知道i發(fā)生了變化,所以繼續(xù)執(zhí)行賣票指令,以至于賣出不存在的票 (i成為負(fù)數(shù))。對(duì)于一個(gè)真實(shí)的售票系統(tǒng)來說,這將成為一個(gè)嚴(yán)重的錯(cuò)誤 (售出了過多的票,火車爆滿)。
在并發(fā)情況下,指令執(zhí)行的先后順序由內(nèi)核決定。同一個(gè)線程內(nèi)部,指令按照先后順序執(zhí)行,但不同線程之間的指令很難說清除哪一個(gè)會(huì)先執(zhí)行。如果運(yùn)行的結(jié)果依賴于不同線程執(zhí)行的先后的話,那么就會(huì)造成競(jìng)爭(zhēng)條件(race condition),在這樣的狀況下,計(jì)算機(jī)的結(jié)果很難預(yù)知。我們應(yīng)該盡量避免競(jìng)爭(zhēng)條件的形成。最常見的解決競(jìng)爭(zhēng)條件的方法是將原先分離的兩個(gè)指令構(gòu)成不可分隔的一個(gè)原子操作(atomic operation),而其它任務(wù)不能插入到原子操作中。
?
多線程同步
對(duì)于多線程程序來說,同步(synchronization)是指在一定的時(shí)間內(nèi)只允許某一個(gè)線程訪問某個(gè)資源 。而在此時(shí)間內(nèi),不允許其它的線程訪問該資源。我們可以通過互斥鎖(mutex),條件變量(condition variable)和讀寫鎖(reader-writer lock)來同步資源。
?
1) 互斥鎖
互斥鎖是一個(gè)特殊的變量,它有鎖上(lock)和打開(unlock)兩個(gè)狀態(tài)。互斥鎖一般被設(shè)置成全局變量。打開的互斥鎖可以由某個(gè)線程獲得。一旦獲得,這個(gè)互斥鎖會(huì)鎖上,此后只有該線程有權(quán)打開。其它想要獲得互斥鎖的線程,會(huì)等待直到互斥鎖再次打開的時(shí)候。我們可以將互斥鎖想像成為一個(gè)只能容納一個(gè)人的洗手間,當(dāng)某個(gè)人進(jìn)入洗手間的時(shí)候,可以從里面將洗手間鎖上。其它人只能在互斥鎖外面等待那個(gè)人出來,才能進(jìn)去。在外面等候的人并沒有排隊(duì),誰先看到洗手間空了,就可以首先沖進(jìn)去。
上面的問題很容易使用互斥鎖的問題解決,每個(gè)線程的程序可以改為:
/*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è)線程依然使用原先的程序 (即不嘗試獲得mu,而直接修改i),互斥鎖不能阻止該程序修改i,互斥鎖就失去了保護(hù)資源的意義。所以,互斥鎖機(jī)制需要程序員自己來寫出完善的程序來實(shí)現(xiàn)互斥鎖的功能。我們下面講的其它機(jī)制也是如此。
?
2) 條件變量
條件變量是另一種常用的變量。它也常常被保存為全局變量,并和互斥鎖合作。
?
假設(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);
上面使用了條件變量。條件變量除了要和互斥鎖配合之外,還需要和另一個(gè)全局變量配合(這里的num, 也就是裝修好的房間數(shù))。這個(gè)全局變量用來構(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的通知。這樣的話,符合條件的線程就開始等待。
當(dāng)有通知(第十個(gè)房間已經(jīng)修建好)到達(dá)的時(shí)候,condwait()會(huì)再次鎖上mu。線程的恢復(fù)運(yùn)行,執(zhí)行下一句prinft("drink beer")?(喝啤酒!)。從這里開始,直到mutex_unlock(),就構(gòu)成了另一個(gè)互斥鎖結(jié)構(gòu)。
那么,前面十個(gè)調(diào)用cond_wait()的線程如何得到的通知呢?我們注意到elif if,即修建好第11個(gè)房間的人,負(fù)責(zé)調(diào)用cond_broadcast()。這個(gè)函數(shù)會(huì)給所有調(diào)用cond_wait()的線程放送通知,以便讓那些線程恢復(fù)運(yùn)行。
?
條件變量特別適用于多個(gè)線程等待某個(gè)條件的發(fā)生。如果不使用條件變量,那么每個(gè)線程就需要不斷嘗試獲得互斥鎖并檢查條件是否發(fā)生,這樣大大浪費(fèi)了系統(tǒng)的資源。
?
3) 讀寫鎖
讀寫鎖與互斥鎖非常相似。r、RW lock有三種狀態(tài):?共享讀取鎖(shared-read),?互斥寫入鎖(exclusive-write lock),?打開(unlock)。后兩種狀態(tài)與之前的互斥鎖兩種狀態(tài)完全相同。
一個(gè)unlock的RW lock可以被某個(gè)線程獲取R鎖或者W鎖。
如果被一個(gè)線程獲得R鎖,RW lock可以被其它線程繼續(xù)獲得R鎖,而不必等待該線程釋放R鎖。但是,如果此時(shí)有其它線程想要獲得W鎖,它必須等到所有持有共享讀取鎖的線程釋放掉各自的R鎖。
如果一個(gè)鎖被一個(gè)線程獲得W鎖,那么其它線程,無論是想要獲取R鎖還是W鎖,都必須等待該線程釋放W鎖。
這樣,多個(gè)線程就可以同時(shí)讀取共享資源。而具有危險(xiǎn)性的寫入操作則得到了互斥鎖的保護(hù)。
?
我們需要同步并發(fā)系統(tǒng),這為程序員編程帶來了難度。但是多線程系統(tǒng)可以很好的解決許多IO瓶頸的問題。比如我們監(jiān)聽網(wǎng)絡(luò)端口。如果我們只有一個(gè)線程,那么我們必須監(jiān)聽,接收請(qǐng)求,處理,回復(fù),再監(jiān)聽。如果我們使用多線程系統(tǒng),則可以讓多個(gè)線程監(jiān)聽。當(dāng)我們的某個(gè)線程進(jìn)行處理的時(shí)候,我們還可以有其他的線程繼續(xù)監(jiān)聽,這樣,就大大提高了系統(tǒng)的利用率。在數(shù)據(jù)越來越大,服務(wù)器讀寫操作越來越多的今天,這具有相當(dāng)?shù)囊饬x。多線程還可以更有效地利用多CPU的環(huán)境。
(就像做飯一樣,不斷切換去處理不同的菜。)
?
本文中的程序采用偽C的寫法。不同的語言有不同的函數(shù)名(比如mutex_lock)。這里關(guān)注的是邏輯上的概念,而不是具體的實(shí)現(xiàn)和語言規(guī)范。
?
總結(jié)
multiple threads, multiple stacks
race condition
mutex, condition variable, RW lock
評(píng)論
查看更多