現(xiàn)代軟件系統(tǒng)中状您,除了進(jìn)程傀蓉,線程也是一個(gè)非常重要的概念欧漱。隨著CPU頻率增長(zhǎng)開(kāi)始出現(xiàn)停滯,處理器逐漸開(kāi)始想多核方向發(fā)展葬燎。多線程误甚,作為實(shí)現(xiàn)軟件并發(fā)執(zhí)行的重要方法之一,也開(kāi)始被重視谱净。
線程基礎(chǔ)
線程概念
線程(Thread)窑邦,也稱輕量級(jí)進(jìn)程(Lightweight Process,LWP)壕探,是程序執(zhí)行流的最小單元冈钦。一個(gè)標(biāo)準(zhǔn)的線程由線程ID、當(dāng)前指令指針(PC)李请、寄存器集合瞧筛、堆棧組成厉熟。通常,一個(gè)進(jìn)程由一個(gè)到多個(gè)線程組成驾窟,各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段庆猫、數(shù)據(jù)段、堆等)以及一些進(jìn)程級(jí)的資源(如打開(kāi)文件和信號(hào))绅络。
同一進(jìn)程中的多個(gè)線程可以互不干擾地并發(fā)執(zhí)行月培,并且共享進(jìn)程的全局變量和堆的數(shù)據(jù)。相對(duì)于單線程進(jìn)程恩急,使用多線程的原因有一下幾點(diǎn):
- 某個(gè)操作可能會(huì)陷入長(zhǎng)時(shí)間等待杉畜,等待的線程會(huì)進(jìn)入睡眠狀態(tài),無(wú)法繼續(xù)執(zhí)行衷恭。多線程執(zhí)行可以有效利用等待時(shí)間進(jìn)行線程切換此叠。如等待網(wǎng)絡(luò)響應(yīng)。
- 某個(gè)操作可能會(huì)消耗大量的時(shí)間随珠,如果只有一個(gè)線程灭袁,程序和用戶之間的交互會(huì)被中斷。多線程可以讓一個(gè)線程負(fù)責(zé)交互窗看,另一個(gè)線程負(fù)責(zé)計(jì)算茸歧。
- 程序本身要求并發(fā)操作,如一個(gè)多端下載軟件(如Bittorrent)显沈。
- 多CPU或多核處理器软瞎,本身具備同時(shí)執(zhí)行多個(gè)線程的能力,因此單線程程序無(wú)法全面發(fā)揮計(jì)算機(jī)的全部計(jì)算能力拉讯。
- 相對(duì)于多進(jìn)程應(yīng)用涤浇,多線程在數(shù)據(jù)共享方面效率更高。
線程訪問(wèn)權(quán)限
線程可以訪問(wèn)進(jìn)程內(nèi)存中的所有的數(shù)據(jù)魔慷,包括如下幾個(gè)方面:
- 全局變量
- 堆數(shù)據(jù)
- 函數(shù)里的靜態(tài)變量
- 程序代碼只锭,任何線程都有權(quán)利讀取并執(zhí)行任何代碼
- 打開(kāi)的文件,A線程打開(kāi)的文件可以由B線程讀取
當(dāng)然實(shí)際上線程也擁有自己的私有存儲(chǔ)空間院尔,包括如下幾個(gè)方面:
- 棧:盡管并非完全無(wú)法被其他線程訪問(wèn)纹烹,但是一般情況下還是認(rèn)為棧是私有數(shù)據(jù)。
- 線程局部存儲(chǔ)(Thread Local Storage召边,TLS):線程局部存儲(chǔ)是某些操作系統(tǒng)為線程單獨(dú)提供的私有空間铺呵,容量有限。
- 寄存器:寄存器是執(zhí)行流的基本數(shù)據(jù)隧熙,為線程私有片挂。
線程調(diào)度與優(yōu)先級(jí)
當(dāng)線程數(shù)量小于等于處理器數(shù)量時(shí)(并且操作系統(tǒng)支持多處理器),線程的并發(fā)是真正的并發(fā),不同的線程運(yùn)行在不同的處理器上音念。當(dāng)線程數(shù)據(jù)大于處理器數(shù)量時(shí)沪饺,此時(shí)至少有一個(gè)處理器會(huì)運(yùn)行多個(gè)線程。
在單處理器運(yùn)行多線程情況下闷愤,并發(fā)是一種模擬出來(lái)的狀態(tài)整葡。操作系統(tǒng)會(huì)讓這些多線程輪流執(zhí)行,每次僅執(zhí)行一小段時(shí)間(通常是幾十到幾百毫秒)讥脐,這樣每個(gè)線程“看起來(lái)”在同時(shí)執(zhí)行遭居。這樣一個(gè)不斷在處理器上切換不同的線程的行為稱為 線程調(diào)度。
在線程調(diào)度中旬渠,線程通常擁有至少三種狀態(tài)俱萍,分別是:
- 運(yùn)行(Running):此時(shí)線程正在執(zhí)行。
- 就緒(Ready):此時(shí)線程可以立刻運(yùn)行告丢,但CPI已被占用枪蘑。
- 等待(Waiting):此時(shí)線程正在等待某一事件(通常是I/O或同步)發(fā)生,無(wú)法執(zhí)行岖免。
處于運(yùn)行中的線程擁有一段可以執(zhí)行的時(shí)間岳颇,這段時(shí)間稱為 時(shí)間片(Time Slice)。當(dāng)時(shí)間片用盡時(shí)颅湘,線程進(jìn)入就緒狀態(tài)话侧。如果線程在時(shí)間片用盡前就開(kāi)始等待某事件,則它將進(jìn)入等待狀態(tài)栅炒。當(dāng)一個(gè)線程離開(kāi)運(yùn)行狀態(tài)時(shí)掂摔,系統(tǒng)會(huì)選擇一個(gè)處于就緒狀態(tài)的線程繼續(xù)執(zhí)行术羔。在一個(gè)處于等待狀態(tài)的線程所等待的事件發(fā)生后赢赊,該線程將進(jìn)入就緒狀態(tài)。如下圖所示為線程的狀態(tài)轉(zhuǎn)移圖级历。
在線程調(diào)度中释移,主要有兩種調(diào)度算法:
- 優(yōu)先級(jí)調(diào)度(Priority Schedule):線程擁有各自的 線程優(yōu)先級(jí)(Thread Priority),高優(yōu)先級(jí)的線程會(huì)更早執(zhí)行寥殖,低優(yōu)先級(jí)的線程需要等待系統(tǒng)中沒(méi)有高優(yōu)先級(jí)的可執(zhí)行線程存在時(shí)才能執(zhí)行玩讳。
- 輪轉(zhuǎn)調(diào)度(Round Robin Schedule):讓各個(gè)線程輪流執(zhí)行一段時(shí)間片。
實(shí)際應(yīng)用中嚼贡,系統(tǒng)還會(huì)根據(jù)不同線程的表現(xiàn)自動(dòng)調(diào)整優(yōu)先級(jí)熏纯,提高線程調(diào)度效率。在系統(tǒng)中粤策,一般把頻繁等待的線程稱為 IO密集型線程(IO Bound Thread)樟澜;把很少等待的線程稱為 CPU密集型線程(CPU Bound Thread)。通常,IO 密集型線程比 CPU 密集型線程更容易得到優(yōu)先級(jí)的提升秩贰。
在優(yōu)先級(jí)調(diào)度中霹俺,存在一種 餓死(Starvation) 現(xiàn)象,即一個(gè)線程的優(yōu)先級(jí)較低毒费,在它執(zhí)行之前丙唧,總是有較高優(yōu)先級(jí)的線程在它之前執(zhí)行。當(dāng)一個(gè)CPU密集型的線程獲得較高優(yōu)先級(jí)時(shí)觅玻,許多低優(yōu)先級(jí)的進(jìn)程就可能餓死想际。當(dāng)一個(gè)IO密集型的線程獲得較高優(yōu)先級(jí)時(shí),由于大部分之間處于等待狀態(tài)串塑,因此相對(duì)不容易造成其他線程餓死沼琉。為了避免餓死現(xiàn)象,調(diào)度系統(tǒng)通常會(huì)逐步提升那些等待時(shí)間過(guò)長(zhǎng)且未得到執(zhí)行的線程的優(yōu)先級(jí)桩匪。
可搶占線程和不可搶占線程
輪轉(zhuǎn)調(diào)度中打瘪,線程在用盡時(shí)間片后會(huì)被強(qiáng)制剝奪繼續(xù)執(zhí)行的權(quán)利,而進(jìn)入就緒狀態(tài)傻昙,該過(guò)程稱為 搶占(Preemption)闺骚。在早期的一些操作系統(tǒng)中,線程是不可搶占的妆档。在這種調(diào)度模型下僻爽,線程必須主動(dòng)進(jìn)入就緒狀態(tài),而不是靠時(shí)間片用盡來(lái)被強(qiáng)制進(jìn)入贾惦。如果線程始終拒絕進(jìn)入就緒狀態(tài)胸梆,并且不進(jìn)行任何等待操作,其他線程將永遠(yuǎn)無(wú)法執(zhí)行须板。
在不可搶占線程中碰镜,線程會(huì)在兩種情況下主動(dòng)放棄執(zhí)行:
- 當(dāng)線程試圖等待某些事件時(shí)(如I/O事件)。
- 線程主動(dòng)放棄時(shí)間片习瑰。
線程安全
多線程程序處于一個(gè)多變的環(huán)境中绪颖,可以訪問(wèn)的全局變量和堆數(shù)據(jù)隨時(shí)都可能被其他的線程改變。因此甜奄,多線程程序在并發(fā)時(shí)數(shù)據(jù)的一致性非常重要柠横。
競(jìng)爭(zhēng)與原子操作
多線程同時(shí)訪問(wèn)一個(gè)共享數(shù)據(jù),可能會(huì)造成嚴(yán)重的后果课兄。以一個(gè)著名的例子為例牍氛,假設(shè)有兩個(gè)線程分別執(zhí)行如下所示的 C 代碼。
// 線程 1
i = 1;
++i;
// 線程 2
--i;
在很多體系結(jié)構(gòu)中烟阐,++i 的實(shí)現(xiàn)方式一般如下:
- 讀取 i 到某個(gè)寄存器 X
- X++
- 將 X 的內(nèi)容存儲(chǔ)至 i
由于線程 1 和線程 2并發(fā)執(zhí)行搬俊,因此兩個(gè)線程的執(zhí)行可能如下(注意,寄存器 X 的 內(nèi)容在不同的線程中是不一樣的,這里用 X[1] 和 X[2] 分別表示線程 1 和線程 2 中的 X)悠抹,如下所示:
執(zhí)行序號(hào) | 執(zhí)行指令 | 語(yǔ)句執(zhí)行后的變量值 | 線程 |
---|---|---|---|
1 | i = 1 | i = 1, X[1] = 未知 | 1 |
2 | X[1] = i | i = 1, X[1] = 1 | 1 |
3 | X[2] = i | i = 1, X[2] = 1 | 2 |
4 | X[1]++ | i = 1, X[1] = 2 | 1 |
5 | X[2]-- | i = 1, X[2] = 0 | 2 |
6 | i = X[1] | i = 2, X[1] = 2 | 1 |
7 | i = X[2] | i = 0, X[2] = 0 | 2 |
從程序邏輯上看珠月,兩個(gè)線程都執(zhí)行完畢之后,i 的值應(yīng)該是 1楔敌,但從表中的執(zhí)行序列可以看到啤挎,i 的實(shí)際值是 0。實(shí)際上卵凑,這兩個(gè)線程如果同時(shí)執(zhí)行庆聘,i 的結(jié)果有可能是 0 或 1 或 2。
很明顯勺卢,由于 i++ 操作在多線程環(huán)境下會(huì)出現(xiàn)錯(cuò)誤是因?yàn)樵摬僮鞅痪幾g成匯編代碼后不止一條指令伙判,因此在執(zhí)行時(shí)可能會(huì)被調(diào)度系統(tǒng)打斷,去執(zhí)行別的代碼黑忱。通常宴抚,我們把單指令的操作稱為 原子操作,因?yàn)閱螚l指令的執(zhí)行是不會(huì)被打斷的甫煞。很多體系結(jié)構(gòu)都提供了一些常用的原子指令菇曲,如 i386 就有一條 inc 指令可以直接增加一個(gè)內(nèi)存單元值,可以避免上例的錯(cuò)誤情況抚吠。
盡管原子操作指令非常方便常潮,但是它們僅適用于比較簡(jiǎn)單特定的場(chǎng)合。在復(fù)雜的場(chǎng)合下楷力,比如要保證一個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)更改的原子性喊式,原子操作指令就力不從心了。這里需要更加通用的手段:鎖萧朝。
同步與鎖
為了避免多個(gè)線程同時(shí)讀寫同一個(gè)數(shù)據(jù)而產(chǎn)生不可預(yù)料的后果岔留,需要將各個(gè)線程對(duì)同一個(gè)數(shù)據(jù)的訪問(wèn)進(jìn)行 同步(Synchronization)。即在一個(gè)線程訪問(wèn)數(shù)據(jù)未結(jié)束時(shí)剪勿,其他線程不得對(duì)同一個(gè)數(shù)據(jù)進(jìn)行訪問(wèn)贸诚。
同步最常見(jiàn)的方法是使用 鎖(Lock)方庭。鎖是一種非強(qiáng)制機(jī)制厕吉,每個(gè)線程在訪問(wèn)數(shù)據(jù)或資源之前首先試圖 獲取(Acquire) 鎖械念,并在訪問(wèn)結(jié)束之后 釋放(Release) 鎖头朱。在鎖已經(jīng)被占用時(shí)試圖獲取鎖時(shí),線程會(huì)等待龄减,直到鎖重新可用项钮。
二元信號(hào)量
二元信號(hào)量(Binary Semaphore) 是最簡(jiǎn)單的一種鎖,只有兩種狀態(tài):占用、非占用烁巫。二元信號(hào)量適合只能被唯一一個(gè)線程獨(dú)占訪問(wèn)的資源署隘。當(dāng)二元信號(hào)量處于非占用狀態(tài)時(shí),第一個(gè)試圖獲取該二元信號(hào)量的線程會(huì)獲得該鎖亚隙,并將二元信號(hào)量置為占用狀態(tài)磁餐,此后其他所有試圖獲取該二元信號(hào)量的線程將會(huì)等待,直到該鎖被釋放阿弃。
多元信號(hào)量
多元信號(hào)量是二元信號(hào)量的擴(kuò)展诊霹,簡(jiǎn)稱 信號(hào)量(Semaphore)。一個(gè)初始值為 N 的信號(hào)量允許 N 個(gè)線程并發(fā)訪問(wèn)渣淳。
當(dāng)線程訪問(wèn)資源時(shí)脾还,首先獲取信號(hào)量,進(jìn)行如下操作:
- 將信號(hào)量減 1入愧。
- 如果信號(hào)量的值小于 0鄙漏,則進(jìn)入等待狀態(tài),否則繼續(xù)執(zhí)行棺蛛。
當(dāng)線程結(jié)束訪問(wèn)資源后泥张,線程釋放信號(hào)量,進(jìn)行如下操作:
- 將信號(hào)量的值加 1鞠值。
- 如果信號(hào)量的值小于 1媚创,喚醒一個(gè)等待中的線程。
互斥量
互斥量(Mutex) 和二元信號(hào)量很相似彤恶,資源僅同時(shí)允許一個(gè)線程訪問(wèn)钞钙,但和信號(hào)量不同的是:信號(hào)量在整個(gè)系統(tǒng)中可以被任意線程獲取并釋放,即同一個(gè)信號(hào)量可以被系統(tǒng)中的一個(gè)線程獲取之后由另一個(gè)線程釋放声离;互斥量則要求哪個(gè)線程獲取了互斥量芒炼,哪個(gè)線程就要負(fù)責(zé)釋放這個(gè)鎖。
臨界區(qū)
臨界區(qū)(Read-Write Lock) 是比互斥量更加嚴(yán)格的同步手段术徊。在術(shù)語(yǔ)中本刽,把臨界區(qū)的鎖的獲取稱為進(jìn)入臨界區(qū),而把鎖的釋放稱為離開(kāi)臨界區(qū)赠涮。臨界區(qū)和互斥量與信號(hào)量的區(qū)別在于:互斥量和信號(hào)量在系統(tǒng)的任何進(jìn)程里都是可見(jiàn)的子寓,即一個(gè)進(jìn)程創(chuàng)建了一個(gè)互斥量或信號(hào)量,另一個(gè)進(jìn)程試圖獲取該鎖是合法的笋除;臨界區(qū)的作用范圍僅限于本進(jìn)程斜友,其他的進(jìn)程無(wú)法獲取該鎖。
讀寫鎖
讀寫鎖(Read-Write Lock) 致力于一種更加特定的場(chǎng)合的同步垃它。對(duì)于一段數(shù)據(jù)鲜屏,多個(gè)線程同時(shí)讀取總是沒(méi)有問(wèn)題的烹看,但假設(shè)操作都不是原子型,只要有任何一個(gè)線程試圖對(duì)該數(shù)據(jù)進(jìn)行修改洛史,就必須使用同步手段來(lái)避免出錯(cuò)惯殊。對(duì)此,可以使用上述的信號(hào)量也殖、互斥量或臨界區(qū)中的任何一種來(lái)進(jìn)行同步靠胜。雖然這樣可以保證程序正確執(zhí)行,但是對(duì)于讀取頻繁的程序毕源,會(huì)顯得非常低效浪漠。讀寫鎖就是用來(lái)提高這種情況下的執(zhí)行效率的。
讀寫鎖有兩種獲取方式:共享的(Shared)霎褐、獨(dú)占的(Exclusive)址愿。當(dāng)鎖處于自由狀態(tài)時(shí),試圖以任何一種方式獲取鎖都能成功冻璃,并將鎖置于對(duì)應(yīng)的狀態(tài)响谓。如果鎖處于共享狀態(tài),其他線程以共享的方式獲取鎖仍然會(huì)成功省艳,此時(shí)這個(gè)鎖分配給了多個(gè)線程娘纷。如果其他線程試圖以獨(dú)占的方式獲取已經(jīng)處于共享狀態(tài)的鎖,那么它必須等待鎖被所有線程釋放跋炕。處于獨(dú)占狀態(tài)的鎖將阻止任何其他線程獲取該鎖赖晶。
條件變量
條件變量(Condition Variable) 作為一種同步手段,作用類似于一個(gè)柵欄辐烂。對(duì)于條件變量遏插,線程可以有兩種操作:首先,線程可以等待條件變量纠修,一個(gè)條件變量可以被多個(gè)線程等待胳嘲。其次,線程可以喚醒條件變量扣草,此時(shí)所有等待此條件變量的線程都會(huì)被喚醒并繼續(xù)執(zhí)行了牛。
線程模型
線程的并發(fā)執(zhí)行是由多處理器或操作系統(tǒng)調(diào)度來(lái)實(shí)現(xiàn)的。但實(shí)際情況要更為復(fù)雜:大多數(shù)操作系統(tǒng)辰妙,包括 Windows 和 Linux鹰祸,都在內(nèi)核里提供線程的支持,內(nèi)核態(tài)線程由多處理器或調(diào)度來(lái)實(shí)現(xiàn)并發(fā)上岗。然而用戶實(shí)際使用的線程并不是內(nèi)核態(tài)線程福荸,而是用戶態(tài)線程蕴坪。用戶態(tài)線程并不一定在操作系統(tǒng)內(nèi)核里對(duì)應(yīng)同等數(shù)量的內(nèi)核態(tài)線程肴掷。它們之間的對(duì)應(yīng)關(guān)系有三種類型敬锐。
一對(duì)一模型
對(duì)于直接支持線程的系統(tǒng),一對(duì)一模型始終是最簡(jiǎn)單的模型呆瞻。對(duì)于一對(duì)一模型台夺,一個(gè)用戶態(tài)線程唯一對(duì)應(yīng)一個(gè)內(nèi)核態(tài)線程,但一個(gè)內(nèi)核態(tài)線程并不一定存在相應(yīng)的用戶態(tài)線程痴脾。模型示意圖如下所示颤介。
一對(duì)一模型中,用戶態(tài)線程具有和內(nèi)核態(tài)線程一致的優(yōu)點(diǎn)赞赖,線程之間的并發(fā)是真正的并發(fā)滚朵,一個(gè)線程因?yàn)槟吃蜃枞麜r(shí),其他線程的執(zhí)行不會(huì)受到影響前域。此外辕近,一對(duì)一模型也可以讓多線程程序在多處理器的系統(tǒng)上有更高的效率。
一對(duì)一線程模型也有兩個(gè)缺點(diǎn):
- 由于許多操作系統(tǒng)限制了內(nèi)核態(tài)線程的數(shù)量匿垄,因此一對(duì)一線程會(huì)讓用戶態(tài)線程的數(shù)量受到限制移宅。
- 許多操作系統(tǒng)內(nèi)核線程調(diào)度時(shí),上下文切換的開(kāi)銷較大椿疗,導(dǎo)致用戶線程的執(zhí)行效率下降漏峰。
多對(duì)一模型
多對(duì)一模型將多個(gè)用戶態(tài)線程映射到一個(gè)內(nèi)核態(tài)線程上,線程之間的切換由用戶態(tài)的代碼完成届榄。因此相對(duì)于一對(duì)一模型浅乔,多對(duì)一模型的切換要快速許多。模型示意圖如下所示铝条。
多對(duì)一模型的問(wèn)題在于:如果其中一個(gè)用戶態(tài)線程阻塞童擎,將導(dǎo)致所有線程都無(wú)法執(zhí)行。另外攻晒,在多處理器系統(tǒng)中顾复,處理器的增多對(duì)于多對(duì)一模型的線程性能也不會(huì)有明顯的提升。多對(duì)一模型的優(yōu)點(diǎn)在于高效的上下文切換和幾乎無(wú)限制的線程數(shù)量鲁捏。
多對(duì)多模型
多對(duì)多模型結(jié)合了多對(duì)一模型和一對(duì)一模型的特點(diǎn)芯砸,將多個(gè)用戶態(tài)線程映射到少數(shù)但不止一個(gè)內(nèi)核態(tài)線程。模型示意圖如下所示给梅。
多對(duì)多模型中假丧,一個(gè)用戶態(tài)線程阻塞并不會(huì)導(dǎo)致所有的用戶態(tài)線程阻塞,因?yàn)榇藭r(shí)還有其他的線程可以被調(diào)度來(lái)執(zhí)行动羽。此外包帚,多對(duì)多模型對(duì)用戶線程的數(shù)量也沒(méi)什么限制,在多處理器系統(tǒng)中运吓,多對(duì)多模型的線程也能得到一定的性能提升渴邦,但是提升幅度步入一對(duì)一模型疯趟。
參考
- 《程序員的自我修養(yǎng)——鏈接、裝載與庫(kù)》
- 《深入理解計(jì)算機(jī)系統(tǒng)》
(完)