Linux下的多線(xiàn)程編程
1 引言
本文引用地址:http://dyxdggzs.com/article/148956.htm線(xiàn)程(thread)技術(shù)早在60年代就被提出,但真正應用多線(xiàn)程到操作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的 Unix也支持線(xiàn)程的概念,但是在一個(gè)進(jìn)程(process)中只允許有一個(gè)線(xiàn)程,這樣多線(xiàn)程就意味著(zhù)多進(jìn)程?,F在,多線(xiàn)程技術(shù)已經(jīng)被許多操作系統所支持,包括Windows/NT,當然,也包括Linux。
為什么有了進(jìn)程的概念后,還要再引入線(xiàn)程呢?使用多線(xiàn)程到底有哪些好處?什么的系統應該選用多線(xiàn)程?我們首先必須回答這些問(wèn)題。
使用多線(xiàn)程的理由之一是和進(jìn)程相比,它是一種非常節儉的多任務(wù)操作方式。我們知道,在Linux系統下,啟動(dòng)一個(gè)新的進(jìn)程必須分配給它獨立的地址空間,建立眾多的數據表來(lái)維護它的代碼段、堆棧段和數據段,這是一種昂貴的多任務(wù)工作方式。而運行于一個(gè)進(jìn)程中的多個(gè)線(xiàn)程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動(dòng)一個(gè)線(xiàn)程所花費的空間遠遠小于啟動(dòng)一個(gè)進(jìn)程所花費的空間,而且,線(xiàn)程間彼此切換所需的時(shí)間也遠遠小于進(jìn)程間切換所需要的時(shí)間。據統計,總的說(shuō)來(lái),一個(gè)進(jìn)程的開(kāi)銷(xiāo)大約是一個(gè)線(xiàn)程開(kāi)銷(xiāo)的30倍左右,當然,在具體的系統上,這個(gè)數據可能會(huì )有較大的區別。
使用多線(xiàn)程的理由之二是線(xiàn)程間方便的通信機制。對不同進(jìn)程來(lái)說(shuō),它們具有獨立的數據空間,要進(jìn)行數據的傳遞只能通過(guò)通信的方式進(jìn)行,這種方式不僅費時(shí),而且很不方便。線(xiàn)程則不然,由于同一進(jìn)程下的線(xiàn)程之間共享數據空間,所以一個(gè)線(xiàn)程的數據可以直接為其它線(xiàn)程所用,這不僅快捷,而且方便。當然,數據的共享也帶來(lái)其他一些問(wèn)題,有的變量不能同時(shí)被兩個(gè)線(xiàn)程所修改,有的子程序中聲明為static的數據更有可能給多線(xiàn)程程序帶來(lái)災難性的打擊,這些正是編寫(xiě)多線(xiàn)程程序時(shí)最需要注意的地方。
除了以上所說(shuō)的優(yōu)點(diǎn)外,不和進(jìn)程比較,多線(xiàn)程程序作為一種多任務(wù)、并發(fā)的工作方式,當然有以下的優(yōu)點(diǎn):
1) 提高應用程序響應。這對圖形界面的程序尤其有意義,當一個(gè)操作耗時(shí)很長(cháng)時(shí),整個(gè)系統都會(huì )等待這個(gè)操作,此時(shí)程序不會(huì )響應鍵盤(pán)、鼠標、菜單的操作,而使用多線(xiàn)程技術(shù),將耗時(shí)長(cháng)的操作(time consuming)置于一個(gè)新的線(xiàn)程,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。操作系統會(huì )保證當線(xiàn)程數不大于CPU數目時(shí),不同的線(xiàn)程運行于不同的CPU上。
3) 改善程序結構。一個(gè)既長(cháng)又復雜的進(jìn)程可以考慮分為多個(gè)線(xiàn)程,成為幾個(gè)獨立或半獨立的運行部分,這樣的程序會(huì )利于理解和修改。
下面我們先來(lái)嘗試編寫(xiě)一個(gè)簡(jiǎn)單的多線(xiàn)程程序。
2 簡(jiǎn)單的多線(xiàn)程編程
Linux系統下的多線(xiàn)程遵循POSIX線(xiàn)程接口,稱(chēng)為pthread。編寫(xiě)Linux下的多線(xiàn)程程序,需要使用頭文件pthread.h,連接時(shí)需要使用庫libpthread.a。順便說(shuō)一下,Linux下pthread的實(shí)現是通過(guò)系統調用clone()來(lái)實(shí)現的。clone()是Linux所特有的系統調用,它的使用方式類(lèi)似fork,關(guān)于clone()的詳細情況,有興趣的讀者可以去查看有關(guān)文檔說(shuō)明。下面我們展示一個(gè)最簡(jiǎn)單的多線(xiàn)程程序 example1.c。
/* example.c*/
#include
#include
void thread(void)
{
int i;
for(i=0;i3;i++)
printf(This is a pthread.n);
}
int main(void)
{
pthread_t id;
int i,ret;
ret=pthread_create(id,NULL,(void *) thread,NULL);
if(ret!=0){
printf (Create pthread error!n);
exit (1);
}
for(i=0;i3;i++)
printf(This is the main process.n);
pthread_join(id,NULL);
return (0);
}
我們編譯此程序:
gcc example1.c -lpthread -o example1
運行example1,我們得到如下結果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
再次運行,我們可能得到如下結果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前后兩次結果不一樣,這是兩個(gè)線(xiàn)程爭奪CPU資源的結果。上面的示例中,我們使用到了兩個(gè)函數, pthread_create和pthread_join,并聲明了一個(gè)pthread_t型的變量。
pthread_t在頭文件/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個(gè)線(xiàn)程的標識符。函數pthread_create用來(lái)創(chuàng )建一個(gè)線(xiàn)程,它的原型為:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第一個(gè)參數為指向線(xiàn)程標識符的指針,第二個(gè)參數用來(lái)設置線(xiàn)程屬性,第三個(gè)參數是線(xiàn)程運行函數的起始地址,最后一個(gè)參數是運行函數的參數。這里,我們的函數thread不需要參數,所以最后一個(gè)參數設為空指針。第二個(gè)參數我們也設為空指針,這樣將生成默認屬性的線(xiàn)程。對線(xiàn)程屬性的設定和修改我們將在下一節闡述。當創(chuàng )建線(xiàn)程成功時(shí),函數返回0,若不為0則說(shuō)明創(chuàng )建線(xiàn)程失敗,常見(jiàn)的錯誤返回代碼為EAGAIN和EINVAL。前者表示系統限制創(chuàng )建新的線(xiàn)程,例如線(xiàn)程數目過(guò)多了;后者表示第二個(gè)參數代表的線(xiàn)程屬性值非法。創(chuàng )建線(xiàn)程成功后,新創(chuàng )建的線(xiàn)程則運行參數三和參數四確定的函數,原來(lái)的線(xiàn)程則繼續運行下一行代碼。
函數pthread_join用來(lái)等待一個(gè)線(xiàn)程的結束。函數原型為:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一個(gè)參數為被等待的線(xiàn)程標識符,第二個(gè)參數為一個(gè)用戶(hù)定義的指針,它可以用來(lái)存儲被等待線(xiàn)程的返回值。這個(gè)函數是一個(gè)線(xiàn)程阻塞的函數,調用它的函數將一直等待到被等待的線(xiàn)程結束為止,當函數返回時(shí),被等待線(xiàn)程的資源被收回。一個(gè)線(xiàn)程的結束有兩種途徑,一種是象我們上面的例子一樣,函數結束了,調用它的線(xiàn)程也就結束了;另一種方式是通過(guò)函數pthread_exit來(lái)實(shí)現。它的函數原型為:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的參數是函數的返回代碼,只要pthread_join中的第二個(gè)參數thread_return不是NULL,這個(gè)值將被傳遞給 thread_return。最后要說(shuō)明的是,一個(gè)線(xiàn)程不能被多個(gè)線(xiàn)程等待,否則第一個(gè)接收到信號的線(xiàn)程成功返回,其余調用pthread_join的線(xiàn)程則返回錯誤代碼ESRCH。
在這一節里,我們編寫(xiě)了一個(gè)最簡(jiǎn)單的線(xiàn)程,并掌握了最常用的三個(gè)函數pthread_create,pthread_join和pthread_exit。下面,我們來(lái)了解線(xiàn)程的一些常用屬性以及如何設置這些屬性。
3 修改線(xiàn)程的屬性
在上一節的例子里,我們用pthread_create函數創(chuàng )建了一個(gè)線(xiàn)程,在這個(gè)線(xiàn)程中,我們使用了默認參數,即將該函數的第二個(gè)參數設為NULL。的確,對大多數程序來(lái)說(shuō),使用默認屬性就夠了,但我們還是有必要來(lái)了解一下線(xiàn)程的有關(guān)屬性。
屬性結構為pthread_attr_t,它同樣在頭文件/usr/include/pthread.h中定義,喜歡追根問(wèn)底的人可以自己去查看。屬性值不能直接設置,須使用相關(guān)函數進(jìn)行操作,初始化的函數為pthread_attr_init,這個(gè)函數必須在pthread_create函數之前調用。屬性對象主要包括是否綁定、是否分離、堆棧地址、堆棧大小、優(yōu)先級。默認的屬性為非綁定、非分離、缺省1M的堆棧、與父進(jìn)程同樣級別的優(yōu)先級。
關(guān)于線(xiàn)程的綁定,牽涉到另外一個(gè)概念:輕進(jìn)程(LWP:Light Weight Process)。輕進(jìn)程可以理解為內核線(xiàn)程,它位于用戶(hù)層和系統層之間。系統對線(xiàn)程資源的分配、對線(xiàn)程的控制是通過(guò)輕進(jìn)程來(lái)實(shí)現的,一個(gè)輕進(jìn)程可以控制一個(gè)或多個(gè)線(xiàn)程。默認狀況下,啟動(dòng)多少輕進(jìn)程、哪些輕進(jìn)程來(lái)控制哪些線(xiàn)程是由系統來(lái)控制的,這種狀況即稱(chēng)為非綁定的。綁定狀況下,則顧名思義,即某個(gè)線(xiàn)程固定的綁在一個(gè)輕進(jìn)程之上。被綁定的線(xiàn)程具有較高的響應速度,這是因為CPU時(shí)間片的調度是面向輕進(jìn)程的,綁定的線(xiàn)程可以保證在需要的時(shí)候它總有一個(gè)輕進(jìn)程可用。通過(guò)設置被綁定的輕進(jìn)程的優(yōu)先級和調度級可以使得綁定的線(xiàn)程滿(mǎn)足諸如實(shí)時(shí)反應之類(lèi)的要求。
設置線(xiàn)程綁定狀態(tài)的函數為pthread_attr_setscope,它有兩個(gè)參數,第一個(gè)是指向屬性結構的指針,第二個(gè)是綁定類(lèi)型,它有兩個(gè)取值:PTHREAD_SCOPE_SYSTEM(綁定的)和PTHREAD_SCOPE_PROCESS(非綁定的)。下面的代碼即創(chuàng )建了一個(gè)綁定的線(xiàn)程。
#include
pthread_attr_t attr;
pthread_t tid;
/*初始化屬性值,均設為默認值*/
pthread_attr_init(attr);
pthread_attr_setscope(attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(tid, attr, (void *) my_function, NULL);
線(xiàn)程的分離狀態(tài)決定一個(gè)線(xiàn)程以什么樣的方式來(lái)終止自己。在上面的例子中,我們采用了線(xiàn)程的默認屬性,即為非分離狀態(tài),這種情況下,原有的線(xiàn)程等待創(chuàng )建的線(xiàn)程結束。只有當pthread_join()函數返回時(shí),創(chuàng )建的線(xiàn)程才算終止,才能釋放自己占用的系統資源。而分離線(xiàn)程不是這樣子的,它沒(méi)有被其他的線(xiàn)程所等待,自己運行結束了,線(xiàn)程也就終止了,馬上釋放系統資源。程序員應該根據自己的需要,選擇適當的分離狀態(tài)。設置線(xiàn)程分離狀態(tài)的函數為 pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個(gè)參數可選為PTHREAD_CREATE_DETACHED(分離線(xiàn)程)和 PTHREAD _CREATE_JOINABLE(非分離線(xiàn)程)。這里要注意的一點(diǎn)是,如果設置一個(gè)線(xiàn)程為分離線(xiàn)程,而這個(gè)線(xiàn)程運行又非???,它很可能在 pthread_create函數返回之前就終止了,它終止以后就可能將線(xiàn)程號和系統資源移交給其他的線(xiàn)程使用,這樣調用pthread_create的線(xiàn)程就得到了錯誤的線(xiàn)程號。要避免這種情況可以采取一定的同步措施,最簡(jiǎn)單的方法之一是可以在被創(chuàng )建的線(xiàn)程里調用 pthread_cond_timewait函數,讓這個(gè)線(xiàn)程等待一會(huì )兒,留出足夠的時(shí)間讓函數pthread_create返回。設置一段等待時(shí)間,是在多線(xiàn)程編程里常用的方法。但是注意不要使用諸如wait()之類(lèi)的函數,它們是使整個(gè)進(jìn)程睡眠,并不能解決線(xiàn)程同步的問(wèn)題。
另外一個(gè)可能常用的屬性是線(xiàn)程的優(yōu)先級,它存放在結構sched_param中。用函數pthread_attr_getschedparam和函數 pthread_attr_setschedparam進(jìn)行存放,一般說(shuō)來(lái),我們總是先取優(yōu)先級,對取得的值修改后再存放回去。下面即是一段簡(jiǎn)單的例子。
#include
#include
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(attr);
pthread_attr_getschedparam(attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(attr, ¶m);
pthread_create(tid, attr, (void *)myfunction, myarg);
4 線(xiàn)程的數據處理
和進(jìn)程相比,線(xiàn)程的最大優(yōu)點(diǎn)之一是數據的共享性,各個(gè)進(jìn)程共享父進(jìn)程處沿襲的數據段,可以方便的獲得、修改數據。但這也給多線(xiàn)程編程帶來(lái)了許多問(wèn)題。我們必須當心有多個(gè)不同的進(jìn)程訪(fǎng)問(wèn)相同的變量。許多函數是不可重入的,即同時(shí)不能運行一個(gè)函數的多個(gè)拷貝(除非使用不同的數據段)。在函數中聲明的靜態(tài)變量常常帶來(lái)問(wèn)題,函數的返回值也會(huì )有問(wèn)題。因為如果返回的是函數內部靜態(tài)聲明的空間的地址,則在一個(gè)線(xiàn)程調用該函數得到地址后使用該地址指向的數據時(shí),別的線(xiàn)程可能調用此函數并修改了這一段數據。在進(jìn)程中共享的變量必須用關(guān)鍵字volatile來(lái)定義,這是為了防止編譯器在優(yōu)化時(shí)(如gcc中使用-OX參數)改變它們的使用方式。為了保護變量,我們必須使用信號量、互斥等方法來(lái)保證我們對變量的正確使用。下面,我們就逐步介紹處理線(xiàn)程數據時(shí)的有關(guān)知識。
4.1 線(xiàn)程數據
在單線(xiàn)程的程序里,有兩種基本的數據:全局變量和局部變量。但在多線(xiàn)程程序里,還有第三種數據類(lèi)型:線(xiàn)程數據(TSD: Thread-Specific Data)。它和全局變量很象,在線(xiàn)程內部,各個(gè)函數可以象使用全局變量一樣調用它,但它對線(xiàn)程外部的其它線(xiàn)程是不可見(jiàn)的。這種數據的必要性是顯而易見(jiàn)的。例如我們常見(jiàn)的變量errno,它返回標準的出錯信息。它顯然不能是一個(gè)局部變量,幾乎每個(gè)函數都應該可以調用它;但它又不能是一個(gè)全局變量,否則在 A線(xiàn)程里輸出的很可能是B線(xiàn)程的出錯信息。要實(shí)現諸如此類(lèi)的變量,我們就必須使用線(xiàn)程數據。我們?yōu)槊總€(gè)線(xiàn)程數據創(chuàng )建一個(gè)鍵,它和這個(gè)鍵相關(guān)聯(lián),在各個(gè)線(xiàn)程里,都使用這個(gè)鍵來(lái)指代線(xiàn)程數據,但在不同的線(xiàn)程里,這個(gè)鍵代表的數據是不同的,在同一個(gè)線(xiàn)程里,它代表同樣的數據內容。
和線(xiàn)程數據相關(guān)的函數主要有4個(gè):創(chuàng )建一個(gè)鍵;為一個(gè)鍵指定線(xiàn)程數據;從一個(gè)鍵讀取線(xiàn)程數據;刪除鍵。
創(chuàng )建鍵的函數原型為:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
第一個(gè)參數為指向一個(gè)鍵值的指針,第二個(gè)參數指明了一個(gè)destructor函數,如果這個(gè)參數不為空,那么當每個(gè)線(xiàn)程結束時(shí),系統將調用這個(gè)函數來(lái)釋放綁定在這個(gè)鍵上的內存塊。這個(gè)函數常和函數pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個(gè)鍵只被創(chuàng )建一次。函數pthread_once聲明一個(gè)初始化函數,第一次調用pthread_once時(shí)它執行這個(gè)函數,以后的調用將被它忽略。
在下面的例子中,我們創(chuàng )建一個(gè)鍵,并將它和某個(gè)數據相關(guān)聯(lián)。我們要定義一個(gè)函數createWindow,這個(gè)函數定義一個(gè)圖形窗口(數據類(lèi)型為Fl_Window *,這是圖形界面開(kāi)發(fā)工具FLTK中的數據類(lèi)型)。由于各個(gè)線(xiàn)程都會(huì )調用這個(gè)函數,所以我們使用線(xiàn)程數據。
/* 聲明一個(gè)鍵*/
pthread_key_t myWinKey;
/* 函數 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 調用函數createMyKey,創(chuàng )建鍵*/
pthread_once ( once, createMyKey) ;
/*win指向一個(gè)新建立的窗口*/
win=new Fl_Window( 0, 0, 100, 100, MyWindow);
/* 對此窗口作一些可能的設置工作,如大小、位置、名稱(chēng)等*/
setWindow(win);
/* 將窗口指針值綁定在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函數 createMyKey,創(chuàng )建一個(gè)鍵,并指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(myWinKey, freeWinKey);
}
/* 函數 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}
這樣,在不同的線(xiàn)程中調用函數createMyWin,都可以得到在線(xiàn)程內部均可見(jiàn)的窗口變量,這個(gè)變量通過(guò)函數 pthread_getspecific得到。在上面的例子中,我們已經(jīng)使用了函數pthread_setspecific來(lái)將線(xiàn)程數據和一個(gè)鍵綁定在一起。這兩個(gè)函數的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
這兩個(gè)函數的參數意義和使用方法是顯而易見(jiàn)的。要注意的是,用pthread_setspecific為一個(gè)鍵指定新的線(xiàn)程數據時(shí),必須自己釋放原有的線(xiàn)程數據以回收空間。這個(gè)過(guò)程函數pthread_key_delete用來(lái)刪除一個(gè)鍵,這個(gè)鍵占用的內存將被釋放,但同樣要注意的是,它只釋放鍵占用的內存,并不釋放該鍵關(guān)聯(lián)的線(xiàn)程數據所占用的內存資源,而且它也不會(huì )觸發(fā)函數pthread_key_create中定義的destructor函數。線(xiàn)程數據的釋放必須在釋放鍵之前完成。
4.2 互斥鎖
互斥鎖用來(lái)保證一段時(shí)間內只有一個(gè)線(xiàn)程在執行一段代碼。必要性顯而易見(jiàn):假設各個(gè)線(xiàn)程向同一個(gè)文件順序寫(xiě)入數據,最后得到的結果一定是災難性的。
我們先看下面一段代碼。這是一個(gè)讀/寫(xiě)程序,它們公用一個(gè)緩沖區,并且我們假定一個(gè)緩沖區只能保存一條信息。即緩沖區只有兩個(gè)狀態(tài):有信息或沒(méi)有信息。
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void ){
pthread_t reader;
/* 定義延遲時(shí)間*/
delay.tv_sec = 2;
delay.tv_nec = 0;
/* 用默認屬性初始化一個(gè)互斥鎖對象*/
pthread_mutex_init (mutex,NULL);
pthread_create(reader, pthread_attr_default, (void *)reader_function), NULL);
writer_function( );
}
void writer_function (void){
while(1){
/* 鎖定互斥鎖*/
pthread_mutex_lock (mutex);
if (buffer_has_item==0){
buffer=make_new_item( );
buffer_has_item=1;
}
/* 打開(kāi)互斥鎖*/
pthread_mutex_unlock(mutex);
pthread_delay_np(delay);
}
}
void reader_function(void){
while(1){
pthread_mutex_lock(mutex);
if(buffer_has_item==1){
consume_item(buffer);
buffer_has_item=0;
}
pthread_mutex_unlock(mutex);
pthread_delay_np(delay);
}
}
這里聲明了互斥鎖變量mutex,結構pthread_mutex_t為不公開(kāi)的數據類(lèi)型,其中包含一個(gè)系統分配的屬性對象。函數 pthread_mutex_init用來(lái)生成一個(gè)互斥鎖。NULL參數表明使用默認屬性。如果需要聲明特定屬性的互斥鎖,須調用函數 pthread_mutexattr_init。函數pthread_mutexattr_setpshared和函數 pthread_mutexattr_settype用來(lái)設置互斥鎖屬性。前一個(gè)函數設置屬性pshared,它有兩個(gè)取值, PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來(lái)不同進(jìn)程中的線(xiàn)程同步,后者用于同步本進(jìn)程的不同線(xiàn)程。在上面的例子中,我們使用的是默認屬性PTHREAD_PROCESS_ PRIVATE。后者用來(lái)設置互斥鎖類(lèi)型,可選的類(lèi)型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上所、解鎖機制,一般情況下,選用最后一個(gè)默認屬性。
pthread_mutex_lock聲明開(kāi)始用互斥鎖上鎖,此后的代碼直至調用pthread_mutex_unlock為止,均被上鎖,即同一時(shí)間只能被一個(gè)線(xiàn)程調用執行。當一個(gè)線(xiàn)程執行到pthread_mutex_lock處時(shí),如果該鎖此時(shí)被另一個(gè)線(xiàn)程使用,那此線(xiàn)程被阻塞,即程序將等待到另一個(gè)線(xiàn)程釋放此互斥鎖。在上面的例子中,我們使用了pthread_delay_np函數,讓線(xiàn)程睡眠一段時(shí)間,就是為了防止一個(gè)線(xiàn)程始終占據此函數。
上面的例子非常簡(jiǎn)單,就不再介紹了,需要提出的是在使用互斥鎖的過(guò)程中很有可能會(huì )出現死鎖:兩個(gè)線(xiàn)程試圖同時(shí)占用兩個(gè)資源,并按不同的次序鎖定相應的互斥鎖,例如兩個(gè)線(xiàn)程都需要鎖定互斥鎖1和互斥鎖2,a線(xiàn)程先鎖定互斥鎖1,b線(xiàn)程先鎖定互斥鎖2,這時(shí)就出現了死鎖。此時(shí)我們可以使用函數 pthread_mutex_trylock,它是函數pthread_mutex_lock的非阻塞版本,當它發(fā)現死鎖不可避免時(shí),它會(huì )返回相應的信息,程序員可以針對死鎖做出相應的處理。另外不同的互斥鎖類(lèi)型對死鎖的處理不一樣,但最主要的還是要程序員自己在程序設計注意這一點(diǎn)。
4.3 條件變量
前一節中我們講述了如何使用互斥鎖來(lái)實(shí)現線(xiàn)程間數據的共享和通信,互斥鎖一個(gè)明顯的缺點(diǎn)是它只有兩種狀態(tài):鎖定和非鎖定。而條件變量通過(guò)允許線(xiàn)程阻塞和等待另一個(gè)線(xiàn)程發(fā)送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時(shí),條件變量被用來(lái)阻塞一個(gè)線(xiàn)程,當條件不滿(mǎn)足時(shí),線(xiàn)程往往解開(kāi)相應的互斥鎖并等待條件發(fā)生變化。一旦其它的某個(gè)線(xiàn)程改變了條件變量,它將通知相應的條件變量喚醒一個(gè)或多個(gè)正被此條件變量阻塞的線(xiàn)程。這些線(xiàn)程將重新鎖定互斥鎖并重新測試條件是否滿(mǎn)足。一般說(shuō)來(lái),條件變量被用來(lái)進(jìn)行線(xiàn)承間的同步。
條件變量的結構為pthread_cond_t,函數pthread_cond_init()被用來(lái)初始化一個(gè)條件變量。它的原型為:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是一個(gè)指向結構pthread_cond_t的指針,cond_attr是一個(gè)指向結構pthread_condattr_t的指針。結構pthread_condattr_t是條件變量的屬性結構,和互斥鎖一樣我們可以用它來(lái)設置條件變量是進(jìn)程內可用還是進(jìn)程間可用,默認值是 PTHREAD_ PROCESS_PRIVATE,即此條件變量被同一進(jìn)程內的各個(gè)線(xiàn)程使用。注意初始化條件變量只有未被使用時(shí)才能重新初始化或被釋放。釋放一個(gè)條件變量的函數為pthread_cond_ destroy(pthread_cond_t cond)。
函數pthread_cond_wait()使線(xiàn)程阻塞在一個(gè)條件變量上。它的函數原型為:
extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
線(xiàn)程解開(kāi)mutex指向的鎖并被條件變量cond阻塞。線(xiàn)程可以被函數pthread_cond_signal和函數 pthread_cond_broadcast喚醒,但是要注意的是,條件變量只是起阻塞和喚醒線(xiàn)程的作用,具體的判斷條件還需用戶(hù)給出,例如一個(gè)變量是否為0等等,這一點(diǎn)我們從后面的例子中可以看到。線(xiàn)程被喚醒后,它將重新檢查判斷條件是否滿(mǎn)足,如果還不滿(mǎn)足,一般說(shuō)來(lái)線(xiàn)程應該仍阻塞在這里,被等待被下一次喚醒。這個(gè)過(guò)程一般用while語(yǔ)句實(shí)現。
另一個(gè)用來(lái)阻塞線(xiàn)程的函數是pthread_cond_timedwait(),它的原型為:
extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
它比函數pthread_cond_wait()多了一個(gè)時(shí)間參數,經(jīng)歷abstime段時(shí)間后,即使條件變量不滿(mǎn)足,阻塞也被解除。
函數pthread_cond_signal()的原型為:
extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
它用來(lái)釋放被阻塞在條件變量cond上的一個(gè)線(xiàn)程。多個(gè)線(xiàn)程阻塞在此條件變量上時(shí),哪一個(gè)線(xiàn)程被喚醒是由線(xiàn)程的調度策略所決定的。要注意的是,必須用保護條件變量的互斥鎖來(lái)保護這個(gè)函數,否則條件滿(mǎn)足信號又可能在測試條件和調用pthread_cond_wait函數之間被發(fā)出,從而造成無(wú)限制的等待。下面是使用函數pthread_cond_wait()和函數pthread_cond_signal()的一個(gè)簡(jiǎn)單的例子。
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count () {
pthread_mutex_lock (count_lock);
while(count==0)
pthread_cond_wait( count_nonzero, count_lock);
count=count -1;
pthread_mutex_unlock (count_lock);
}
increment_count(){
pthread_mutex_lock(count_lock);
if(count==0)
pthread_cond_signal(count_nonzero);
count=count+1;
pthread_mutex_unlock(count_lock);
}
count值為0時(shí), decrement函數在pthread_cond_wait處被阻塞,并打開(kāi)互斥鎖count_lock。此時(shí),當調用到函數 increment_count時(shí),pthread_cond_signal()函數改變條件變量,告知decrement_count()停止阻塞。讀者可以試著(zhù)讓兩個(gè)線(xiàn)程分別運行這兩個(gè)函數,看看會(huì )出現什么樣的結果。
函數pthread_cond_broadcast(pthread_cond_t *cond)用來(lái)喚醒所有被阻塞在條件變量cond上的線(xiàn)程。這些線(xiàn)程被喚醒后將再次競爭相應的互斥鎖,所以必須小心使用這個(gè)函數。
4.4 信號量
信號量本質(zhì)上是一個(gè)非負的整數計數器,它被用來(lái)控制對公共資源的訪(fǎng)問(wèn)。當公共資源增加時(shí),調用函數sem_post()增加信號量。只有當信號量值大于0時(shí),才能使用公共資源,使用后,函數sem_wait()減少信號量。函數sem_trywait()和函數pthread_ mutex_trylock()起同樣的作用,它是函數sem_wait()的非阻塞版本。下面我們逐個(gè)介紹和信號量有關(guān)的一些函數,它們都在頭文件 /usr/include/semaphore.h中定義。
信號量的數據類(lèi)型為結構sem_t,它本質(zhì)上是一個(gè)長(cháng)整型的數。函數sem_init()用來(lái)初始化一個(gè)信號量。它的原型為:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem為指向信號量結構的一個(gè)指針;pshared不為0時(shí)此信號量在進(jìn)程間共享,否則只能為當前進(jìn)程的所有線(xiàn)程共享;value給出了信號量的初始值。
函數sem_post( sem_t *sem )用來(lái)增加信號量的值。當有線(xiàn)程阻塞在這個(gè)信號量上時(shí),調用這個(gè)函數會(huì )使其中的一個(gè)線(xiàn)程不在阻塞,選擇機制同樣是由線(xiàn)程的調度策略決定的。
函數sem_wait( sem_t *sem )被用來(lái)阻塞當前線(xiàn)程直到信號量sem的值大于0,解除阻塞后將sem的值減一,表明公共資源經(jīng)使用后減少。函數sem_trywait ( sem_t *sem )是函數sem_wait()的非阻塞版本,它直接將信號量sem的值減一。
函數sem_destroy(sem_t *sem)用來(lái)釋放信號量sem。
下面我們來(lái)看一個(gè)使用信號量的例子。在這個(gè)例子中,一共有4個(gè)線(xiàn)程,其中兩個(gè)線(xiàn)程負責從文件讀取數據到公共的緩沖區,另兩個(gè)線(xiàn)程從緩沖區讀取數據作不同的處理(加和乘運算)。
/* File sem.c */
#include
#include
#include
#define MAXSTACK 100
int stack[MAXSTACK][2];
int size=0;
sem_t sem;
/* 從文件1.dat讀取數據,每讀一次,信號量加一*/
void ReadData1(void){
FILE *fp=fopen(1.dat,r);
while(!feof(fp)){
fscanf(fp,%d %d,stack[size][0],stack[size][1]);
sem_post(sem);
++size;
}
fclose(fp);
}
/*從文件2.dat讀取數據*/
void ReadData2(void){
FILE *fp=fopen(2.dat,r);
while(!feof(fp)){
fscanf(fp,%d %d,stack[size][0],stack[size][1]);
sem_post(sem);
++size;
}
fclose(fp);
}
/*阻塞等待緩沖區有數據,讀取數據后,釋放空間,繼續等待*/
void HandleData1(void){
while(1){
sem_wait(sem);
printf(Plus:%d+%d=%dn,stack[size][0],stack[size][1],
stack[size][0]+stack[size][1]);
--size;
}
}
void HandleData2(void){
while(1){
sem_wait(sem);
printf(Multiply:%d*%d=%dn,stack[size][0],stack[size][1],
stack[size][0]*stack[size][1]);
--size;
}
}
int main(void){
pthread_t t1,t2,t3,t4;
sem_init(sem,0,0);
pthread_create(t1,NULL,(void *)HandleData1,NULL);
pthread_create(t2,NULL,(void *)HandleData2,NULL);
pthread_create(t3,NULL,(void *)ReadData1,NULL);
pthread_create(t4,NULL,(void *)ReadData2,NULL);
/* 防止程序過(guò)早退出,讓它在此無(wú)限期等待*/
pthread_join(t1,NULL);
}
在Linux下,我們用命令gcc -lpthread sem.c -o sem生成可執行文件sem。 我們事先編輯好數據文件1.dat和2.dat,假設它們的內容分別為1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我們運行sem,得到如下的結果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
從中我們可以看出各個(gè)線(xiàn)程間的競爭關(guān)系。而數值并未按我們原先的順序顯示出來(lái)這是由于size這個(gè)數值被各個(gè)線(xiàn)程任意修改的緣故。這也往往是多線(xiàn)程編程要注意的問(wèn)題。
5 小結
多線(xiàn)程編程是一個(gè)很有意思也很有用的技術(shù),使用多線(xiàn)程技術(shù)的網(wǎng)絡(luò )螞蟻是目前最常用的下載工具之一,使用多線(xiàn)程技術(shù)的grep比單線(xiàn)程的grep要快上幾倍,類(lèi)似的例子還有很多。希望大家能用多線(xiàn)程技術(shù)寫(xiě)出高效實(shí)用的好程序來(lái)。
linux操作系統文章專(zhuān)題:linux操作系統詳解(linux不再難懂)
評論