Linux線程概述
了解如何正確運(yùn)用線程是每一個(gè)優(yōu)秀程序員必備的素質(zhì)方仿。
線程類似于進(jìn)程筏养。如同進(jìn)程寻咒,線程由內(nèi)核按時(shí)間分片進(jìn)行管理谭胚。在單處理器系統(tǒng)中徐块,內(nèi)核使用時(shí)間分片來(lái)模擬線程的并發(fā)執(zhí)行,這種方式和進(jìn)程的相同灾而。而在多處理器系統(tǒng)中胡控,如同多個(gè)進(jìn)程,線程實(shí)際上一樣可以并發(fā)執(zhí)行旁趟。
那么為什么對(duì)于大多數(shù)合作性任務(wù)昼激,多線程比多個(gè)獨(dú)立的進(jìn)程更優(yōu)越呢?這是因?yàn)槲眩€程共享相同的內(nèi)存空間癣猾。不同的線程可以存取內(nèi)存中的同一個(gè)變量。所以余爆,程序中的所有線程都可以讀或?qū)懧暶鬟^(guò)的全局變量纷宇。如果曾用 fork() 編寫過(guò)重要代碼,就會(huì)認(rèn)識(shí)到這個(gè)工具的重要性蛾方。為什么呢像捶?雖然 fork() 允許創(chuàng)建多個(gè)進(jìn)程上陕,但它還會(huì)帶來(lái)以下通信問題: 如何讓多個(gè)進(jìn)程相互通信,這里每個(gè)進(jìn)程都有各自獨(dú)立的內(nèi)存空間拓春。對(duì)這個(gè)問題沒有一個(gè)簡(jiǎn)單的答案释簿。雖然有許多不同種類的本地 IPC (進(jìn)程間通信),但它們都遇到兩個(gè)重要障礙:強(qiáng)加了某種形式的額外內(nèi)核開銷硼莽,從而降低性能庶溶。
對(duì)于大多數(shù)情形,IPC 不是對(duì)于代碼的“自然”擴(kuò)展懂鸵。通常極大地增加了程序的復(fù)雜性偏螺。雙重壞事: 開銷和復(fù)雜性都非好事。如果曾經(jīng)為了支持 IPC 而對(duì)程序大動(dòng)干戈過(guò)匆光,那么你就會(huì)真正欣賞線程提供的簡(jiǎn)單共享內(nèi)存機(jī)制套像。由于所有的線程都駐留在同一內(nèi)存空間,POSIX 線程無(wú)需進(jìn)行開銷大而復(fù)雜的長(zhǎng)距離調(diào)用终息。只要利用簡(jiǎn)單的同步機(jī)制夺巩,程序中所有的線程都可以讀取和修改已有的數(shù)據(jù)結(jié)構(gòu)。而無(wú)需將數(shù)據(jù)經(jīng)由文件描述符轉(zhuǎn)儲(chǔ)或擠入緊窄的共享內(nèi)存空間周崭。僅此一個(gè)原因柳譬,就足以讓你考慮應(yīng)該采用單進(jìn)程/多線程模式而非多進(jìn)程/單線程模式。
線程是快捷的不僅如此续镇。線程同樣還是非痴饕铮快捷的。與標(biāo)準(zhǔn) fork() 相比磨取,線程帶來(lái)的開銷很小人柿。內(nèi)核無(wú)需單獨(dú)復(fù)制進(jìn)程的內(nèi)存空間或文件描述符等等。這就節(jié)省了大量的 CPU 時(shí)間忙厌,使得線程創(chuàng)建比新進(jìn)程創(chuàng)建快上十到一百倍凫岖。因?yàn)檫@一點(diǎn),可以大量使用線程而無(wú)需太過(guò)于擔(dān)心帶來(lái)的 CPU 或內(nèi)存不足逢净。使用 fork() 時(shí)導(dǎo)致的大量 CPU 占用也不復(fù)存在哥放。這表示只要在程序中有意義,通常就可以創(chuàng)建線程爹土。
當(dāng)然甥雕,和進(jìn)程一樣,線程將利用多 CPU胀茵。如果軟件是針對(duì)多處理器系統(tǒng)設(shè)計(jì)的社露,這就真的是一大特性(如果軟件是開放源碼,則最終可能在不少平臺(tái)上運(yùn)行)琼娘。特定類型線程程序(尤其是 CPU 密集型程序)的性能將隨系統(tǒng)中處理器的數(shù)目幾乎線性地提高峭弟。如果正在編寫 CPU 非常密集型的程序附鸽,則絕對(duì)想設(shè)法在代碼中使用多線程。一旦掌握了線程編碼瞒瘸,無(wú)需使用繁瑣的 IPC 和其它復(fù)雜的通信機(jī)制坷备,就能夠以全新和創(chuàng)造性的方法解決編碼難題。所有這些特性配合在一起使得多線程編程更有趣情臭、快速和靈活省撑。
如果熟悉 Linux 編程,就有可能知道 __clone() 系統(tǒng)調(diào)用俯在。__clone() 類似于 fork()竟秫,同時(shí)也有許多線程的特性。例如朝巫,使用 __clone()鸿摇,新的子進(jìn)程可以有選擇地共享父進(jìn)程的執(zhí)行環(huán)境(內(nèi)存空間石景,文件描述符等)劈猿。這是好的一面。但 __clone() 也有不足之處潮孽。正如__clone() 在線幫助指出:“__clone 調(diào)用是特定于 Linux 平臺(tái)的揪荣,不適用于實(shí)現(xiàn)可移植的程序。欲編寫線程化應(yīng)用程序(多線程控制同一內(nèi)存空間)往史,最好使用實(shí)現(xiàn) POSIX 1003.1c 線程 API 的庫(kù)仗颈,例如 Linux-Threads 庫(kù)。參閱 pthread_create(3thr)椎例“ぞ觯”
雖然 __clone() 有線程的許多特性,但它是不可移植的订歪。當(dāng)然這并不意味著代碼中不能使用它脖祈。但在軟件中考慮使用 __clone() 時(shí)應(yīng)當(dāng)權(quán)衡這一事實(shí)。值得慶幸的是刷晋,正如 __clone() 在線幫助指出盖高,有一種更好的替代方案:POSIX 線程。如果想編寫 可移植的 多線程代碼眼虱,代碼可運(yùn)行于 Solaris喻奥、FreeBSD、Linux 和其它平臺(tái)捏悬,POSIX 線程是一種當(dāng)然之選撞蚕。
相對(duì)進(jìn)程而言,線程是一個(gè)更加接近于執(zhí)行體的概念过牙,它可以與同進(jìn)程中的其他線程共享數(shù)據(jù)诈豌,但擁有自己的椘途龋空間,擁有獨(dú)立的執(zhí)行序列矫渔。在串行程序基礎(chǔ)上引入線程和進(jìn)程是為了提高程序的并發(fā)度彤蔽,從而提高程序運(yùn)行效率和響應(yīng)時(shí)間。
線程和進(jìn)程在使用上各有優(yōu)缺點(diǎn):線程執(zhí)行開銷小庙洼,但不利于資源的管理和保護(hù)顿痪;而進(jìn)程正相反。同時(shí)油够,線程適合于在SMP機(jī)器上運(yùn)行蚁袭,而進(jìn)程則可以跨機(jī)器遷移。
POSIX通過(guò)pthread_create()函數(shù)創(chuàng)建線程石咬,API定義如下:
int? pthread_create(pthread_t? *? thread, pthread_attr_t * attr,
void * (*start_routine)(void *), void * arg)
與fork()調(diào)用創(chuàng)建一個(gè)進(jìn)程的方法不同揩悄,pthread_create()創(chuàng)建的線程并不具備與主線程(即調(diào)用pthread_create()的線程)同樣的執(zhí)行序列,而是使其運(yùn)行start_routine(arg)函數(shù)鬼悠。thread返回創(chuàng)建的線程ID删性,而attr是創(chuàng)建線程時(shí)設(shè)置的線程屬性(見下)。pthread_create()的返回值表示線程創(chuàng)建是否成功焕窝。盡管arg是void *類型的變量蹬挺,但它同樣可以作為任意類型的參數(shù)傳給start_routine()函數(shù);同時(shí)它掂,start_routine()可以返回一個(gè)void *類型的返回值巴帮,而這個(gè)返回值也可以是其他類型,并由pthread_join()獲取虐秋。
pthread_create()中的attr參數(shù)是一個(gè)結(jié)構(gòu)指針榕茧,結(jié)構(gòu)中的元素分別對(duì)應(yīng)著新線程的運(yùn)行屬性,主要包括以下幾項(xiàng):
__detachstate客给,表示新線程是否與進(jìn)程中其他線程脫離同步用押,如果置位則新線程不能用pthread_join()來(lái)同步,且在退出時(shí)自行釋放所占用的資源起愈。缺省為PTHREAD_CREATE_JOINABLE狀態(tài)只恨。這個(gè)屬性也可以在線程創(chuàng)建并運(yùn)行以后用pthread_detach()來(lái)設(shè)置,而一旦設(shè)置為PTHREAD_CREATE_DETACH狀態(tài)(不論是創(chuàng)建時(shí)設(shè)置還是運(yùn)行時(shí)設(shè)置)則不能再恢復(fù)到PTHREAD_CREATE_JOINABLE狀態(tài)抬虽。
__schedpolicy官觅,表示新線程的調(diào)度策略,主要包括SCHED_OTHER(正常阐污、非實(shí)時(shí))休涤、SCHED_RR(實(shí)時(shí)、輪轉(zhuǎn)法)和SCHED_FIFO(實(shí)時(shí)、先入先出)三種功氨,缺省為SCHED_OTHER序苏,后兩種調(diào)度策略僅對(duì)超級(jí)用戶有效。運(yùn)行時(shí)可以用過(guò)pthread_setschedparam()來(lái)改變捷凄。
__schedparam忱详,一個(gè)struct sched_param結(jié)構(gòu),目前僅有一個(gè)sched_priority整型變量表示線程的運(yùn)行優(yōu)先級(jí)跺涤。這個(gè)參數(shù)僅當(dāng)調(diào)度策略為實(shí)時(shí)(即SCHED_RR或SCHED_FIFO)時(shí)才有效匈睁,并可以在運(yùn)行時(shí)通過(guò)pthread_setschedparam()函數(shù)來(lái)改變,缺省為0桶错。
__inheritsched航唆,有兩種值可供選擇:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新線程使用顯式指定調(diào)度策略和調(diào)度參數(shù)(即attr中的值)院刁,而后者表示繼承調(diào)用者線程的值糯钙。缺省為PTHREAD_EXPLICIT_SCHED。
__scope退腥,表示線程間競(jìng)爭(zhēng)CPU的范圍任岸,也就是說(shuō)線程優(yōu)先級(jí)的有效范圍。POSIX的標(biāo)準(zhǔn)中定義了兩個(gè)值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS阅虫,前者表示與系統(tǒng)中所有線程一起競(jìng)爭(zhēng)CPU時(shí)間演闭,后者表示僅與同進(jìn)程中的線程競(jìng)爭(zhēng)CPU不跟。目前LinuxThreads僅實(shí)現(xiàn)了PTHREAD_SCOPE_SYSTEM一值颓帝。
pthread_attr_t結(jié)構(gòu)中還有一些值,但不使用pthread_create()來(lái)設(shè)置窝革。
為了設(shè)置這些屬性购城,POSIX定義了一系列屬性設(shè)置函數(shù),包括pthread_attr_init()虐译、pthread_attr_destroy()和與各個(gè)屬性相關(guān)的pthread_attr_get---/pthread_attr_set---函數(shù)瘪板。
線程創(chuàng)建的Linux實(shí)現(xiàn)
我們知道,Linux的線程實(shí)現(xiàn)是在核外進(jìn)行的漆诽,核內(nèi)提供的是創(chuàng)建進(jìn)程的接口do_fork()侮攀。內(nèi)核提供了兩個(gè)系統(tǒng)調(diào)用__clone()和fork(),最終都用不同的參數(shù)調(diào)用do_fork()核內(nèi)API厢拭。當(dāng)然兰英,要想實(shí)現(xiàn)線程,沒有核心對(duì)多進(jìn)程(其實(shí)是輕量級(jí)進(jìn)程)共享數(shù)據(jù)段的支持是不行的供鸠,因此畦贸,do_fork()提供了很多參數(shù),包括CLONE_VM(共享內(nèi)存空間)、CLONE_FS(共享文件系統(tǒng)信息)薄坏、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信號(hào)句柄表)和CLONE_PID(共享進(jìn)程ID,僅對(duì)核內(nèi)進(jìn)程氛谜,即0號(hào)進(jìn)程有效)叔壤。當(dāng)使用fork系統(tǒng)調(diào)用時(shí),內(nèi)核調(diào)用do_fork()不使用任何共享屬性沈善,進(jìn)程擁有獨(dú)立的運(yùn)行環(huán)境杈绸,而使用pthread_create()來(lái)創(chuàng)建線程時(shí),則最終設(shè)置了所有這些屬性來(lái)調(diào)用__clone(),而這些參數(shù)又全部傳給核內(nèi)的do_fork()矮瘟,從而創(chuàng)建的"進(jìn)程"擁有共享的運(yùn)行環(huán)境瞳脓,只有棧是獨(dú)立的,由__clone()傳入澈侠。
Linux線程在核內(nèi)是以輕量級(jí)進(jìn)程的形式存在的劫侧,擁有獨(dú)立的進(jìn)程表項(xiàng),而所有的創(chuàng)建哨啃、同步烧栋、刪除等操作都在核外pthread庫(kù)中進(jìn)行。pthread庫(kù)使用一個(gè)管理線程(__pthread_manager()拳球,每個(gè)進(jìn)程獨(dú)立且唯一)來(lái)管理線程的創(chuàng)建和終止审姓,為線程分配線程ID,發(fā)送線程相關(guān)的信號(hào)(比如Cancel)祝峻,而主線程(pthread_create())的調(diào)用者則通過(guò)管道將請(qǐng)求信息傳給管理線程魔吐。
一般情況下,線程在其主體函數(shù)退出的時(shí)候會(huì)自動(dòng)終止莱找,但同時(shí)也可以因?yàn)榻邮盏搅硪粋€(gè)線程發(fā)來(lái)的終止(取消)請(qǐng)求而強(qiáng)制終止酬姆。
線程取消的方法是向目標(biāo)線程發(fā)Cancel信號(hào),但如何處理Cancel信號(hào)則由目標(biāo)線程自己決定奥溺,或者忽略辞色、或者立即終止、或者繼續(xù)運(yùn)行至Cancelation-point(取消點(diǎn))浮定,由不同的Cancelation狀態(tài)決定相满。
線程接收到CANCEL信號(hào)的缺省處理(即pthread_create()創(chuàng)建線程的缺省狀態(tài))是繼續(xù)運(yùn)行至取消點(diǎn),也就是說(shuō)設(shè)置一個(gè)CANCELED狀態(tài)桦卒,線程繼續(xù)運(yùn)行立美,只有運(yùn)行至Cancelation-point的時(shí)候才會(huì)退出。
根據(jù)POSIX標(biāo)準(zhǔn)闸盔,pthread_join()悯辙、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()躲撰、sem_wait()针贬、sigwait()等函數(shù)以及read()、write()等會(huì)引起阻塞的系統(tǒng)調(diào)用都是Cancelation-point拢蛋,而其他pthread函數(shù)都不會(huì)引起Cancelation動(dòng)作桦他。但是pthread_cancel的手冊(cè)頁(yè)聲稱,由于LinuxThread庫(kù)與C庫(kù)結(jié)合得不好谆棱,因而目前C庫(kù)函數(shù)都不是Cancelation-point快压;但CANCEL信號(hào)會(huì)使線程從阻塞的系統(tǒng)調(diào)用中退出,并置EINTR錯(cuò)誤碼垃瞧,因此可以在需要作為Cancelation-point的系統(tǒng)調(diào)用前后調(diào)用pthread_testcancel()蔫劣,從而達(dá)到POSIX標(biāo)準(zhǔn)所要求的目標(biāo),即如下代碼段:
? ? pthread_testcancel();
????retcode = read(fd, buffer, length);
????pthread_testcancel();
如果線程處于無(wú)限循環(huán)中个从,且循環(huán)體內(nèi)沒有執(zhí)行至取消點(diǎn)的必然路徑脉幢,則線程無(wú)法由外部其他線程的取消請(qǐng)求而終止。因此在這樣的循環(huán)體的必經(jīng)路徑上應(yīng)該加入pthread_testcancel()調(diào)用嗦锐。
int pthread_cancel(pthread_t thread)?發(fā)送終止信號(hào)給thread線程嫌松,如果成功則返回0,否則為非0值奕污。發(fā)送成功并不意味著thread會(huì)終止萎羔。
int pthread_setcancelstate(int state, int *oldstate)?設(shè)置本線程對(duì)Cancel信號(hào)的反應(yīng),state有兩種值:PTHREAD_CANCEL_ENABLE(缺侍寄)和PTHREAD_CANCEL_DISABLE贾陷,分別表示收到信號(hào)后設(shè)為CANCLED狀態(tài)和忽略CANCEL信號(hào)繼續(xù)運(yùn)行;old_state如果不為NULL則存入原來(lái)的Cancel狀態(tài)以便恢復(fù)腻窒。
int pthread_setcanceltype(int type, int *oldtype)?設(shè)置本線程取消動(dòng)作的執(zhí)行時(shí)機(jī)昵宇,type由兩種取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS磅崭,僅當(dāng)Cancel狀態(tài)為Enable時(shí)有效儿子,分別表示收到信號(hào)后繼續(xù)運(yùn)行至下一個(gè)取消點(diǎn)再退出和立即執(zhí)行取消動(dòng)作(退出);oldtype如果不為NULL則存入運(yùn)來(lái)的取消動(dòng)作類型值砸喻。
void pthread_testcancel(void)?檢查本線程是否處于Canceld狀態(tài)柔逼,如果是,則進(jìn)行取消動(dòng)作割岛,否則直接返回愉适。
下面是一個(gè) POSIX 線程的簡(jiǎn)單示例程序:
#include<pthread.h>??
#include<stdlib.h>
#include<unistd.h>
void?*thread_function(void?*arg)?{??
int?i;??
for?(?i=0;?i<20;?i++)?{??
printf("Thread?says?hi!\n");??
????sleep(1);??
??}??
return?NULL;??
}??
int?main(void)?{??
??pthread_t?mythread;??
if?(?pthread_create(?&mythread,?NULL,?thread_function,?NULL)?)?{??
printf("error?creating?thread.");??
????abort();??
??}??
if?(?pthread_join?(?mythread,?NULL?)?)?{??
printf("error?joining?thread.");??
????abort();??
??}??
??exit(0);??
}??
要編譯這個(gè)程序,只需先將程序存為 thread1.c癣漆,然后輸入:
$ gcc thread1.c -o thread1 -lpthread
運(yùn)行則輸入:
$ ./thread1
理解 thread1.c
thread1.c 是一個(gè)非常簡(jiǎn)單的線程程序维咸。雖然它沒有實(shí)現(xiàn)什么有用的功能,但可以幫助理解線程的運(yùn)行機(jī)制。
下面癌蓖,我們一步一步地了解這個(gè)程序是干什么的瞬哼。
main() 中聲明了變量 mythread,類型是 pthread_t租副。pthread_t 類型在 pthread.h 中定義坐慰,通常稱為“線程 id”(縮寫為 "tid")∮蒙可以認(rèn)為它是一種線程句柄结胀。mythread 聲明后(記住 mythread 只是一個(gè) "tid",或是將要?jiǎng)?chuàng)建的線程的句柄)责循,調(diào)用 pthread_create 函數(shù)創(chuàng)建一個(gè)真實(shí)活動(dòng)的線程糟港。不要因?yàn)?pthread_create() 在 "if" 語(yǔ)句內(nèi)而受其迷惑。由于 pthread_create() 執(zhí)行成功時(shí)返回零而失敗時(shí)則返回非零值院仿,將 pthread_create() 函數(shù)調(diào)用放在 if() 語(yǔ)句中只是為了方便地檢測(cè)失敗的調(diào)用着逐。讓我們查看一下 pthread_create 參數(shù)。第一個(gè)參數(shù) &mythread 是指向 mythread 的指針意蛀。第二個(gè)參數(shù)當(dāng)前為 NULL耸别,可用來(lái)定義線程的某些屬性。由于缺省的線程屬性是適用的县钥,只需將該參數(shù)設(shè)為 NULL秀姐。
第三個(gè)參數(shù)是新線程啟動(dòng)時(shí)調(diào)用的函數(shù)名。本例中若贮,函數(shù)名為 thread_function()省有。當(dāng) thread_function() 返回時(shí),新線程將終止谴麦。本例中蠢沿,線程函數(shù)沒有實(shí)現(xiàn)大的功能。它僅將 "Thread says hi!" 輸出 20 次然后退出匾效。注意 thread_function() 接受 void * 作為參數(shù)舷蟀,同時(shí)返回值的類型也是 void *。這表明可以用 void * 向新線程傳遞任意類型的數(shù)據(jù)面哼,新線程完成時(shí)也可返回任意類型的數(shù)據(jù)野宜。那如何向線程傳遞一個(gè)任意參數(shù)?很簡(jiǎn)單魔策。只要利用 pthread_create() 中的第四個(gè)參數(shù)匈子。本例中,因?yàn)闆]有必要將任何數(shù)據(jù)傳給微不足道的 thread_function()闯袒,所以將第四個(gè)參數(shù)設(shè)為 NULL虎敦。
也許已推測(cè)到游岳,在 pthread_create() 成功返回之后,程序?qū)瑑蓚€(gè)線程其徙。等一等吭历, 兩個(gè) 線程?我們不是只創(chuàng)建了一個(gè)線程嗎擂橘?不錯(cuò)晌区,我們只創(chuàng)建了一個(gè)進(jìn)程。但是主程序同樣也是一個(gè)線程通贞±嗜簦可以這樣理解:如果編寫的程序根本沒有使用 POSIX 線程,則該程序是單線程的(這個(gè)單線程稱為“主”線程)昌罩。創(chuàng)建一個(gè)新線程之后程序總共就有兩個(gè)線程了哭懈。
我想此時(shí)大家至少有兩個(gè)重要問題。第一個(gè)問題茎用,新線程創(chuàng)建之后主線程如何運(yùn)行遣总。答案,主線程按順序繼續(xù)執(zhí)行下一行程序(本例中執(zhí)行 "if (pthread_join(...))")轨功。第二個(gè)問題旭斥,新線程結(jié)束時(shí)如何處理。答案古涧,新線程先停止垂券,然后作為其清理過(guò)程的一部分,等待與另一個(gè)線程合并或“連接”羡滑。
現(xiàn)在菇爪,來(lái)看一下 pthread_join()。正如 pthread_create() 將一個(gè)線程拆分為兩個(gè)柒昏, pthread_join() 將兩個(gè)線程合并為一個(gè)線程凳宙。pthread_join() 的第一個(gè)參數(shù)是 tid mythread。第二個(gè)參數(shù)是指向 void 指針的指針职祷。如果 void 指針不為 NULL氏涩,pthread_join 將線程的 void * 返回值放置在指定的位置上。由于我們不必理會(huì) thread_function() 的返回值堪旧,所以將其設(shè)為 NULL削葱。
你會(huì)注意到 thread_function() 花了 20 秒才完成。在 thread_function() 結(jié)束很久之前淳梦,主線程就已經(jīng)調(diào)用了 pthread_join()。如果發(fā)生這種情況昔字,主線程將中斷(轉(zhuǎn)向睡眠)然后等待 thread_function() 完成爆袍。當(dāng) thread_function() 完成后, pthread_join() 將返回首繁。這時(shí)程序又只有一個(gè)主線程。當(dāng)程序退出時(shí)陨囊,所有新線程已經(jīng)使用 pthread_join() 合并了弦疮。這就是應(yīng)該如何處理在程序中創(chuàng)建的每個(gè)新線程的過(guò)程。如果沒有合并一個(gè)新線程蜘醋,則它仍然對(duì)系統(tǒng)的最大線程數(shù)限制不利胁塞。這意味著如果未對(duì)線程做正確的清理,最終會(huì)導(dǎo)致 pthread_create() 調(diào)用失敗压语。
無(wú)父啸罢,無(wú)子。
如果使用過(guò) fork() 系統(tǒng)調(diào)用胎食,可能熟悉父進(jìn)程和子進(jìn)程的概念扰才。當(dāng)用 fork() 創(chuàng)建另一個(gè)新進(jìn)程時(shí),新進(jìn)程是子進(jìn)程厕怜,原始進(jìn)程是父進(jìn)程衩匣。這創(chuàng)建了可能非常有用的層次關(guān)系,尤其是等待子進(jìn)程終止時(shí)粥航。例如琅捏,waitpid() 函數(shù)讓當(dāng)前進(jìn)程等待所有子進(jìn)程終止。waitpid() 用來(lái)在父進(jìn)程中實(shí)現(xiàn)簡(jiǎn)單的清理過(guò)程递雀。
而 POSIX 線程就更有意思午绳。你可能已經(jīng)注意到我一直有意避免使用“父線程”和“子線程”的說(shuō)法。這是因?yàn)?POSIX 線程中不存在這種層次關(guān)系映之。雖然主線程可以創(chuàng)建一個(gè)新線程拦焚,新線程可以創(chuàng)建另一個(gè)新線程,POSIX 線程標(biāo)準(zhǔn)將它們視為等同的層次杠输。所以等待子線程退出的概念在這里沒有意義赎败。POSIX 線程標(biāo)準(zhǔn)不記錄任何“家族”信息。缺少家族信息有一個(gè)主要含意:如果要等待一個(gè)線程終止蠢甲,就必須將線程的 tid 傳遞給pthread_join()僵刮。線程庫(kù)無(wú)法為你斷定 tid(ps -efL|grep xxx或者top -Hp可以查看到開啟的線程信息)。
對(duì)大多數(shù)開發(fā)者來(lái)說(shuō)這不是個(gè)好消息鹦牛,因?yàn)檫@會(huì)使有多個(gè)線程的程序復(fù)雜化搞糕。不過(guò)不要為此擔(dān)憂。POSIX 線程標(biāo)準(zhǔn)提供了有效地管理多個(gè)線程所需要的所有工具曼追。實(shí)際上窍仰,沒有父/子關(guān)系這一事實(shí)卻為在程序中使用線程開辟了更創(chuàng)造性的方法。例如礼殊,如果有一個(gè)線程稱為線程 1驹吮,線程 1 創(chuàng)建了稱為線程 2 的線程针史,則線程 1 自己沒有必要調(diào)用 pthread_join() 來(lái)合并線程 2,程序中其它任一線程都可以做到碟狞。當(dāng)編寫大量使用線程的代碼時(shí)啄枕,這就可能允許發(fā)生有趣的事情。例如族沃,可以創(chuàng)建一個(gè)包含所有已停止線程的全局“死線程列表”频祝,然后讓一個(gè)專門的清理線程專等停止的線程加到列表中。這個(gè)清理線程調(diào)用 pthread_join() 將剛停止的線程與自己合并〈嘌停現(xiàn)在常空,僅用一個(gè)線程就巧妙和有效地處理了全部清理。
同步漫游
現(xiàn)在我們來(lái)看一些代碼未辆,這些代碼做了一些意想不到的事情窟绷。thread2.c 的代碼如下:
#include<pthread.h>??
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>???
int?myglobal;??
void?*thread_function(void?*arg)?{??
int?i,j;??
for?(?i=0;?i<20;?i++)?{??
????j=myglobal;??
????j=j+1;??
????sleep(1);??
????myglobal=j;??
??}??
return?NULL;??
}??
int?main(void)?{??
??pthread_t?mythread;??
int?i;??
if?(?pthread_create(?&mythread,?NULL,?thread_function,?NULL)?)?{??
printf("error?creating?thread.");??
????abort();??
??}??
for?(?i=0;?i<20;?i++)?{??
????myglobal=myglobal+1;??
????sleep(1);??
??}??
if?(?pthread_join?(?mythread,?NULL?)?)?{??
printf("error?joining?thread.");??
????abort();??
??}??
printf("\nmyglobal?equals?%d\n",myglobal);??
??exit(0);??
}??
理解 thread2.c
如同第一個(gè)程序,這個(gè)程序創(chuàng)建一個(gè)新線程咐柜。主線程和新線程都將全局變量 myglobal 加一 20 次兼蜈。但是程序本身產(chǎn)生了某些意想不到的結(jié)果。編譯代碼請(qǐng)輸入:
$ gcc thread2.c -o thread2 -lpthread
運(yùn)行請(qǐng)輸入:
$ ./thread2
輸出:
$ ./thread2
myglobal equals 21
非常意外吧拙友!因?yàn)?myglobal 從零開始为狸,主線程和新線程各自對(duì)其進(jìn)行了 20 次加一, 程序結(jié)束時(shí) myglobal 值應(yīng)當(dāng)?shù)扔?40。由于 myglobal 輸出結(jié)果為 21遗契,這其中肯定有問題辐棒。但是究竟是什么呢?
放棄嗎牍蜂?好漾根,讓我來(lái)解釋是怎么一回事。首先查看函數(shù) thread_function()鲫竞。注意如何將 myglobal 復(fù)制到局部變量 "j" 了嗎? 接著將 j 加一, 再睡眠一秒辐怕,然后到這時(shí)才將新的 j 值復(fù)制到 myglobal?這就是關(guān)鍵所在从绘。設(shè)想一下寄疏,如果主線程就在新線程將 myglobal 值復(fù)制給 j 后 立即將 myglobal 加一,會(huì)發(fā)生什么僵井?當(dāng) thread_function() 將 j 的值寫回 myglobal 時(shí)陕截,就覆蓋了主線程所做的修改。
當(dāng)編寫線程程序時(shí)批什,應(yīng)避免產(chǎn)生這種無(wú)用的副作用农曲,否則只會(huì)浪費(fèi)時(shí)間(當(dāng)然,除了編寫關(guān)于 POSIX 線程的文章時(shí)有用)渊季。那么朋蔫,如何才能排除這種問題呢罚渐?
由于是將 myglobal 復(fù)制給 j 并且等了一秒之后才寫回時(shí)產(chǎn)生問題却汉,可以嘗試避免使用臨時(shí)局部變量并直接將 myglobal 加一驯妄。雖然這種解決方案對(duì)這個(gè)特定例子適用,但它還是不正確合砂。如果我們對(duì) myglobal 進(jìn)行相對(duì)復(fù)雜的數(shù)學(xué)運(yùn)算青扔,而不是簡(jiǎn)單的加一,這種方法就會(huì)失效翩伪。但是為什么呢微猖?
要理解這個(gè)問題,必須記住線程是并發(fā)運(yùn)行的缘屹。即使在單處理器系統(tǒng)上運(yùn)行(內(nèi)核利用時(shí)間分片模擬多任務(wù))也是可以的凛剥,從程序員的角度,想像兩個(gè)線程是同時(shí)執(zhí)行的轻姿。thread2.c 出現(xiàn)問題是因?yàn)?thread_function() 依賴以下論據(jù):在 myglobal 加一之前的大約一秒鐘期間不會(huì)修改 myglobal犁珠。需要有些途徑讓一個(gè)線程在對(duì) myglobal 做更改時(shí)通知其它線程“不要靠近”。將在下一篇文章中講解如何做到這一點(diǎn)互亮。