但是,現(xiàn)實(shí)中有很多情況下需要在同一個(gè)地址空間中完成并行的任務(wù),比如Web服務(wù)器程序,雖然使用多進(jìn)程方式編程也可以很好地實(shí)現(xiàn)服務(wù)器,但進(jìn)程間的數(shù)據(jù)共享由于需要跨越地址空間而顯得十分不方便,同時(shí)進(jìn)程間切換的開銷也不可小視。
其實(shí)這些問題的本質(zhì)在于兩個(gè)概念:
1. 資源的分組
2. 指令的執(zhí)行流程
所謂資源分組,是指操作系統(tǒng)以什么為最小單位給用戶程序分配資源以及對(duì)這些資源進(jìn)跟蹤。這里提到的資源,指的是打開文件、同步對(duì)象、管道等,以及進(jìn)程最重要的標(biāo)志:地址空間。
在現(xiàn)代操作系統(tǒng)中,進(jìn)程就是所謂的資源分配的最小單位。一個(gè)進(jìn)程擁有自己獨(dú)立的地址空間、內(nèi)核對(duì)象表(記錄打開文件、同步對(duì)象等等)、進(jìn)程句柄等。
而所謂指令流程,實(shí)際上指的是操作系統(tǒng)調(diào)度占用CPU的實(shí)體,我們稱之為“線程”(thread of execution)。
每個(gè)線程擁有自己的用戶棧、核心棧、程序計(jì)數(shù)器等,線程與傳統(tǒng)的進(jìn)程類似,也有運(yùn)行、掛起、就緒等狀態(tài),在狀態(tài)間的轉(zhuǎn)換也類似。但與傳統(tǒng)進(jìn)程所不同的是,線程沒有獨(dú)立的地址空間,所有屬于同一個(gè)進(jìn)程的線程共享同一個(gè)線性地址空間。
由此可知,在線程模型下,操作系統(tǒng)進(jìn)行資源分配是以進(jìn)程為單位,而當(dāng)操作系統(tǒng)進(jìn)行任務(wù)調(diào)度時(shí),則以線程為單位進(jìn)行。當(dāng)然這并不是說進(jìn)程和線程間沒有直接關(guān)系。恰恰相反,進(jìn)程與線程間的關(guān)系非常密切。線程要占用CPU執(zhí)行預(yù)定任務(wù),沒有資源是不可能的完成任務(wù)的;同時(shí),只有資源而沒有指令流的進(jìn)程也是沒有意義的。所以結(jié)果是,一個(gè)進(jìn)程至少包含一個(gè)線程(稱為主線程或初始線程),而一個(gè)線程只屬于一個(gè)進(jìn)程。
線程模型的優(yōu)點(diǎn)
線程的出現(xiàn),使得在同一個(gè)進(jìn)程環(huán)境下進(jìn)行多道程序設(shè)計(jì)成為可能。由于同一個(gè)進(jìn)程所屬的線程間共享同一地址空間,所以線程間可以能過直接傳遞指針來傳遞數(shù)據(jù),而這在傳統(tǒng)的進(jìn)程模型下是不可能實(shí)現(xiàn)的。不單如此,線程間還可以共享內(nèi)核對(duì)象,使得許多任務(wù)得以簡化。
比如,同步對(duì)象的使用。在傳統(tǒng)操作系統(tǒng)中,要將一個(gè)同步對(duì)象的句柄傳遞給另一個(gè)進(jìn)程,有兩條路可以走:通過父子進(jìn)程間的繼承關(guān)系(如Unix中的fork系統(tǒng)調(diào)用)或是通過對(duì)象命名,然后再在另一個(gè)進(jìn)程中以同樣的名字打開。
這兩條路那一條都不是很方便。問題的關(guān)鍵在于,同步對(duì)象的句柄值只是每個(gè)進(jìn)程對(duì)象表中的索引,在另一個(gè)進(jìn)程中是無效的。但在線程模型下,這個(gè)問題就迎刃而解了。因?yàn)椋ㄍ贿M(jìn)程中的)線程間共享同一張內(nèi)核對(duì)象表,所以同一個(gè)同步對(duì)象的句柄對(duì)各線程來說都是有效的,傳遞時(shí)只要直接傳句柄值就行了。
另一個(gè)比較實(shí)際的例子是字處理軟件。假設(shè)現(xiàn)在正在編輯一篇重要文章,為了減少由于斷電而造成的損失,軟件被設(shè)定為每隔1分鐘自動(dòng)存盤一次。
如果在傳統(tǒng)操作系統(tǒng)下,由于一個(gè)進(jìn)程只有一個(gè)執(zhí)行流,每當(dāng)2分鐘的間隔到達(dá)后,進(jìn)程轉(zhuǎn)向響應(yīng)定時(shí)器軟中斷(在Unix下為進(jìn)程收到信號(hào),并執(zhí)行信號(hào)處理過程),這樣所有的處理用戶輸入的代碼被掛起,直至磁盤讀寫完成,信號(hào)處理程序返回為止。如果很不幸地,文章非常長,或者用戶在軟盤或網(wǎng)絡(luò)驅(qū)動(dòng)器上工作,每次保存文章所花時(shí)間為50秒(如果是61秒用戶就幸運(yùn)了,但沒人想要這樣的幸運(yùn)),那么用戶幾乎沒有時(shí)間去編輯文章,這樣的軟件特性顯然毫無用處。
其實(shí),在用戶等待磁盤操作完成的時(shí)候,雖然進(jìn)程對(duì)用戶的輸入無響應(yīng),但CPU確實(shí)是空閑的(假定沒有忙碌的后臺(tái)進(jìn)程),理論上CPU應(yīng)該可以響應(yīng)用戶輸入。這樣,我們就回到了多任務(wù)系統(tǒng)的設(shè)計(jì)初衷:提高CPU利用率。
我們先來討論兩個(gè)不使用線程模型的解決方案:多進(jìn)程編程和使用異步系統(tǒng)調(diào)用。
如果使用多進(jìn)程方式,則由主進(jìn)程新建一個(gè)工作進(jìn)程,將需要保存的數(shù)據(jù)傳遞給工作進(jìn)程以進(jìn)行保存操作。如果需要保存的數(shù)據(jù)量非常大,內(nèi)存間的數(shù)據(jù)復(fù)制是一個(gè)可觀的開銷。當(dāng)然,在較新的操作系統(tǒng)如System V中,由于采用COW(Copy On Write)技術(shù),這個(gè)性能損失可以略過。另一個(gè)改進(jìn)辦法是使用共享內(nèi)存,在一些不使用fork方式新建進(jìn)程的操作系統(tǒng)上這是個(gè)好辦法。
若使用異步系統(tǒng)調(diào)用,則需要編寫一系列信號(hào)處理程序。主程序在運(yùn)行時(shí)跟蹤并記錄當(dāng)前狀態(tài),在信號(hào)出現(xiàn)時(shí)轉(zhuǎn)到信號(hào)處理程序,處理完成后根據(jù)處理前的狀態(tài)繼續(xù)運(yùn)行。這種方案采用的是有限狀態(tài)自動(dòng)機(jī)的思想,可以避免多進(jìn)程操作時(shí)的同步及數(shù)據(jù)傳遞問題,但它使得程序變得相當(dāng)復(fù)雜。
這兩種辦法都是可行的,但前者通訊開銷比較大,后者如果運(yùn)行在多CPU主機(jī)上,則無法充分利用CPU資源。
若使用線程模型,則沒有上述兩個(gè)問題。字處理進(jìn)程可以采用兩個(gè)線程,前后界面線程和后臺(tái)工作線程。界面線程負(fù)責(zé)響應(yīng)用戶輸入,工作線程平進(jìn)處于掛起狀態(tài),并且由主線程定時(shí)把它喚醒進(jìn)行數(shù)據(jù)保存工作。這樣,用戶可以在幾乎無察覺的情況下定時(shí)保存文檔。
線程的實(shí)現(xiàn)
由上文的定義,線程為進(jìn)程中的一個(gè)或多個(gè)指令執(zhí)行流,這個(gè)機(jī)制在現(xiàn)代操作系統(tǒng)的實(shí)現(xiàn)主要可分為兩大類。即根據(jù)操作系統(tǒng)內(nèi)核是否對(duì)線程可感知,分為內(nèi)核線程和用戶線程。
實(shí)際上,上文所說的線程是操作系統(tǒng)調(diào)度的基本單位,實(shí)際上指的只是內(nèi)核線程。所謂內(nèi)核線程,其建立與銷毀都是由操作系統(tǒng)負(fù)責(zé)、通過系統(tǒng)調(diào)用完成的。操作系統(tǒng)在調(diào)度時(shí),參考各進(jìn)程內(nèi)的線程運(yùn)行情況做出調(diào)度決定,如果一個(gè)進(jìn)程中沒有就緒態(tài)的線程,那么這個(gè)進(jìn)程也不會(huì)被調(diào)度占用CPU。
事實(shí)上在Windows 2000中,操作系統(tǒng)進(jìn)行調(diào)度時(shí)根本就不理采線程是屬于哪個(gè)進(jìn)程的,只是將所有的就緒線程統(tǒng)一排成若干個(gè)優(yōu)先級(jí)隊(duì)列,然后進(jìn)行調(diào)度。在這個(gè)情況下,線程的確成了調(diào)度的最小單位,所以有時(shí)線程也被稱為“輕量級(jí)進(jìn)程”。
與內(nèi)核級(jí)線程相對(duì)應(yīng)的,是用戶級(jí)線程。這類實(shí)現(xiàn)多見于一些歷史悠久的操作系統(tǒng)(如Unix系列),為了在操作系統(tǒng)中加入線程支持,采用了在用戶空間增加運(yùn)行庫來實(shí)現(xiàn)線程。這些運(yùn)行庫被稱為“線程包”。
用戶線程是不能被操作系統(tǒng)所感知的,也就是說操作系統(tǒng)還是一如既往地進(jìn)行進(jìn)程調(diào)度,就像根本沒有線程一樣。每當(dāng)用戶進(jìn)程獲得CPU控制權(quán),線程運(yùn)行庫決定該從哪里開始運(yùn)行,即運(yùn)行哪一個(gè)用戶線程。理所當(dāng)然地,各用戶線程之間是非搶占式,一個(gè)用戶線程會(huì)一直運(yùn)行直至它主動(dòng)放棄CPU(線程退出、等待同步對(duì)象或執(zhí)行阻塞式系統(tǒng)調(diào)用)或是整個(gè)進(jìn)程被操作系統(tǒng)重新調(diào)度。
用戶級(jí)線程的優(yōu)點(diǎn)在于它進(jìn)行調(diào)度時(shí)不需要陷入操作系統(tǒng)內(nèi)核,免去了上下文切換的開銷,因而可以達(dá)到較高的性能。
當(dāng)然,實(shí)現(xiàn)用戶級(jí)線程有一些比較復(fù)雜的問題需要解決。
首先,需要處理阻塞式系統(tǒng)調(diào)用。如果沒有采取適當(dāng)?shù)拇胧?,只要某一個(gè)線程執(zhí)行了一個(gè)阻塞式的系統(tǒng)調(diào)用(如Read),則整個(gè)進(jìn)程就會(huì)被操作系統(tǒng)所掛起,直至操作完成,這就違背了線程設(shè)計(jì)的初衷,因?yàn)槠渌€程無法得到CPU的控制權(quán)。
一個(gè)解決方案是使用異步系統(tǒng)調(diào)用進(jìn)行替換。在一些操作系統(tǒng)如Unix中,系統(tǒng)支持異步調(diào)用并且提供查詢系統(tǒng)調(diào)用狀態(tài)的系統(tǒng)調(diào)用(Unix中為Select)。這樣的話,可以對(duì)原來的系統(tǒng)調(diào)用庫進(jìn)行改造,用以實(shí)現(xiàn)線程包。
Select調(diào)用可以查詢當(dāng)前系統(tǒng)調(diào)用(如Read)是否安全,即是否會(huì)發(fā)生阻塞。如果會(huì)發(fā)生阻塞,則線程包的運(yùn)行庫不會(huì)發(fā)出真正的系統(tǒng)調(diào)用,而是把當(dāng)前線程掛起,轉(zhuǎn)而執(zhí)行另一個(gè)線程。然后,在下次運(yùn)行庫獲得控制權(quán)的時(shí)候再次檢查該調(diào)用是否安全,做出是否發(fā)出系統(tǒng)調(diào)用的決定。
這個(gè)解決方案要求線程運(yùn)行庫(run-time)在用戶線程每次進(jìn)程系統(tǒng)調(diào)用的時(shí)候獲取控制權(quán),然后再?zèng)Q定是轉(zhuǎn)發(fā)系統(tǒng)調(diào)用還是進(jìn)行用戶線程調(diào)度。也就說,它得改寫原有的系統(tǒng)調(diào)用的用戶庫,插入用來與線程運(yùn)行庫交互的代碼。這些插入的代碼被稱為外套(jacket)或是封裝器(wrapper)。
其次的問題發(fā)生在出現(xiàn)缺頁異常的時(shí)候。當(dāng)出現(xiàn)缺頁異常時(shí),操作系統(tǒng)會(huì)把通過把外部頁讀入內(nèi)存或是別的什么方法,使發(fā)生異常的頁變?yōu)榭勺x。但問題在于,操作系統(tǒng)并不知道用戶線程的存在,一旦發(fā)生缺頁,操作系統(tǒng)會(huì)掛起整個(gè)進(jìn)程。
另一個(gè)問題就是線程間的非搶占式調(diào)度問題。在進(jìn)程不會(huì)切換的情況下,用戶線程如果不進(jìn)行系統(tǒng)調(diào)用,那么線程運(yùn)行庫就會(huì)不獲得控制權(quán),該線程就會(huì)保持運(yùn)行直至其自愿放棄CPU。這使得用戶線程間不能輪流使用CPU。
一個(gè)可能的解決方案就是由系統(tǒng)傳遞時(shí)鐘中斷到進(jìn)程,即操作系統(tǒng)定時(shí)給線程運(yùn)行庫發(fā)送信號(hào),使其得到控制權(quán)并做定時(shí)調(diào)度成為可能。但這樣做的缺點(diǎn)是開銷過大。
還有的問題就是用戶級(jí)線程無法利用多CPU系統(tǒng)的并行處理能力。
從實(shí)際程序設(shè)計(jì)模型的角度上來看,用戶級(jí)線程也有它的弱點(diǎn)。
現(xiàn)在設(shè)想有一個(gè)多線程的Web服務(wù)器程序,接收網(wǎng)絡(luò)請(qǐng)求并提供服務(wù)。理所當(dāng)然地,它會(huì)產(chǎn)生大量的系統(tǒng)調(diào)用,而且大部分都是阻塞式的。特別是在等待用戶請(qǐng)求的時(shí)候,那些系統(tǒng)調(diào)用都不是在相對(duì)較短的時(shí)間內(nèi)可以解除的。那樣的話,每次線程運(yùn)行庫獲得控制權(quán)時(shí)所執(zhí)行的大量Select大部分都是徒勞無功的。
這就意味著,在一個(gè)系統(tǒng)調(diào)用較少的程序里,用戶線程的性能可以更小一些。但是,在計(jì)算密集的程序里,用多線程方式編程又有多大意義呢?這也成了那些反對(duì)用戶級(jí)線程的人的主要理由之一。
至于內(nèi)核線程,由于它是操作系統(tǒng)的一部分,避免了用戶級(jí)線程所遇到的大量復(fù)雜的實(shí)現(xiàn)性問題。上文提到過,內(nèi)核線程有時(shí)也被稱為輕量級(jí)進(jìn)程,意即它的實(shí)現(xiàn)概念與早期的進(jìn)程有些類似。
內(nèi)核線程的優(yōu)勢在于其實(shí)現(xiàn)的簡潔性,一切工作在內(nèi)核中完成,避免了針對(duì)用戶庫的大量修改。
聯(lián)系客服