1 引言?
線程(thread)技術(shù)早在60年代就被提出,但真正應(yīng)用多線程到操作系統(tǒng)中去,是在80年代中期,solaris是這方面的佼佼者。傳統(tǒng)的Unix也支持線程的概念,但是在一個進程(process)中只允許有一個線程,這樣多線程就意味著多進程。現(xiàn)在,多線程技術(shù)已經(jīng)被許多操作系統(tǒng)所支持,包括Windows/NT,當然,也包括Linux。
為什么有了進程的概念后,還要再引入線程呢?使用多線程到底有哪些好處?什么的系統(tǒng)應(yīng)該選用多線程?我們首先必須回答這些問題。?
使用多線程的理由之一是和進程相比,它是一種非常”節(jié)儉”的多任務(wù)操作方式。我們知道,在Linux系統(tǒng)下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數(shù)據(jù)表來維護它的代碼段、堆棧段和數(shù)據(jù)段,這是一種”昂貴”的多任務(wù)工作方式。而運行于一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數(shù)據(jù),啟動一個線程所花費的空間遠遠小于啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小于進程間切換所需要的時間。據(jù)統(tǒng)計,總的說來桓黿痰目笤際且桓魷叱炭?0倍左右,當然,在具體的系統(tǒng)上,這個數(shù)據(jù)可能會有較大的區(qū)別。?
使用多線程的理由之二是線程間方便的通信機制。對不同進程來說,它們具有獨立的數(shù)據(jù)空間,要進行數(shù)據(jù)的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由于同一進程下的線程之間共享數(shù)據(jù)空間,所以一個線程的數(shù)據(jù)可以直接為其它線程所用,這不僅快捷,而且方便。當然,數(shù)據(jù)的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明為static的數(shù)據(jù)更有可能給多線程程序帶來災(zāi)難性的打擊,這些正是編寫多線程程序時最需要注意的地方。?
除了以上所說的優(yōu)點外,不和進程比較,多線程程序作為一種多任務(wù)、并發(fā)的工作方式,當然有以下的優(yōu)點:?
1) 提高應(yīng)用程序響應(yīng)。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統(tǒng)都會等待這個操作,此時程序不會響應(yīng)鍵盤、鼠標、菜單的操作,而使用多線程技術(shù),將耗時長的操作(time consuming)置于一個新的線程,可以避免這種尷尬的情況。?
2) 使多CPU系統(tǒng)更加有效。操作系統(tǒng)會保證當線程數(shù)不大于CPU數(shù)目時,不同的線程運行于不同的CPU上。?
3) 改善程序結(jié)構(gòu)。一個既長又復(fù)雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利于理解和修改。?
下面我們先來嘗試編寫一個簡單的多線程程序。
2 簡單的多線程編程?
Linux系統(tǒng)下的多線程遵循POSIX線程接口,稱為pthread。編寫Linux下的多線程程序,需要使用頭文件pthread.h,連接時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現(xiàn)是通過系統(tǒng)調(diào)用clone()來實現(xiàn)的。clone()是Linux所特有的系統(tǒng)調(diào)用,它的使用方式類似fork,關(guān)于clone()的詳細情況,有興趣的讀者可以去查看有關(guān)文檔說明。下面我們展示一個最簡單的多線程程序example1.c。
/* example.c*/ #include < stdio.h > #include < pthread.h >void thread(void) { int i; for (i = 0; i < 3; i++) printf("This is a pthread. ");}int main(void) { pthread_t id; int i, ret; ret = pthread_create( & id, NULL, (void * ) thread, NULL); if (ret != 0) { printf("Create pthread error! "); exit(1); } for (i = 0; i < 3; i++) printf("This is the main process. "); pthread_join(id, NULL); return (0);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我們編譯此程序:? gcc example1.c -lpthread -o example1? 運行example1,我們得到如下結(jié)果:? 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.? 再次運行,我們可能得到如下結(jié)果:? 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.
前后兩次結(jié)果不一樣,這是兩個線程爭奪CPU資源的結(jié)果。上面的示例中,我們使用到了兩個函數(shù), pthread_create和pthread_join,并聲明了一個pthread_t型的變量。? pthread_t在頭文件/usr/include/bits/pthreadtypes.h中定義:? typedef unsigned long int pthread_t;? 它是一個線程的標識符。函數(shù)pthread_create用來創(chuàng)建一個線程,它的原型為:? extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,? void?(*__start_routine) (void?), void *__arg));? 第一個參數(shù)為指向線程標識符的指針,第二個參數(shù)用來設(shè)置線程屬性,第三個參數(shù)是線程運行函數(shù)的起始地址,最后一個參數(shù)是運行函數(shù)的參數(shù)。這里,我們的函數(shù)thread不需要參數(shù),所以最后一個參數(shù)設(shè)為空指針。第二個參數(shù)我們也設(shè)為空指針,這樣將生成默認屬性的線程。對線程屬性的設(shè)定和修改我們將在下一節(jié)闡述。當創(chuàng)建線程成功時,函數(shù)返回0,若不為0則說明創(chuàng)建線程失敗,常見的錯誤返回代碼為EAGAIN和EINVAL。前者表示系統(tǒng)限制創(chuàng)建新的線程,例如線程數(shù)目過多了;后者表示第二個參數(shù)代表的線程屬性值非法。創(chuàng)建線程成功后,新創(chuàng)建的線程則運行參數(shù)三和參數(shù)四確定的函數(shù),原來的線程則繼續(xù)運行下一行代碼。? 函數(shù)pthread_join用來等待一個線程的結(jié)束。函數(shù)原型為:? extern int pthread_join __P ((pthread_t __th, void **__thread_return));? 第一個參數(shù)為被等待的線程標識符,第二個參數(shù)為一個用戶定義的指針,它可以用來存儲被等待線程的返回值。這個函數(shù)是一個線程阻塞的函數(shù),調(diào)用它的函數(shù)將一直等待到被等待的線程結(jié)束為止,當函數(shù)返回時,被等待線程的資源被收回。一個線程的結(jié)束有兩種途徑,一種是象我們上面的例子一樣,函數(shù)結(jié)束了,調(diào)用它的線程也就結(jié)束了;另一種方式是通過函數(shù)pthread_exit來實現(xiàn)。它的函數(shù)原型為:? extern void pthread_exit?P ((void *__retval)) __attribute?((noreturn));? 唯一的參數(shù)是函數(shù)的返回代碼,只要pthread_join中的第二個參數(shù)thread_return不是NULL,這個值將被傳遞給thread_return。最后要說明的是,一個線程不能被多個線程等待,否則第一個接收到信號的線程成功返回,其余調(diào)用pthread_join的線程則返回錯誤代碼ESRCH。? 在這一節(jié)里,我們編寫了一個最簡單的線程,并掌握了最常用的三個函數(shù)pthread_create,pthread_join和pthread_exit。下面,我們來了解線程的一些常用屬性以及如何設(shè)置這些屬性。
3 修改線程的屬性? 在上一節(jié)的例子里,我們用pthread_create函數(shù)創(chuàng)建了一個線程,在這個線程中,我們使用了默認參數(shù),即將該函數(shù)的第二個參數(shù)設(shè)為NULL。的確,對大多數(shù)程序來說,使用默認屬性就夠了,但我們還是有必要來了解一下線程的有關(guān)屬性。? 屬性結(jié)構(gòu)為pthread_attr_t,它同樣在頭文件/usr/include/pthread.h中定義,喜歡追根問底的人可以自己去查看。屬性值不能直接設(shè)置,須使用相關(guān)函數(shù)進行操作,初始化的函數(shù)為pthread_attr_init,這個函數(shù)必須在pthread_create函數(shù)之前調(diào)用。屬性對象主要包括是否綁定、是否分離、堆棧地址、堆棧大小、優(yōu)先級。默認的屬性為非綁定、非分離、缺省1M的堆棧、與父進程同樣級別的優(yōu)先級。? 關(guān)于線程的綁定,牽涉到另外一個概念:輕進程(LWP:Light Weight Process)。輕進程可以理解為內(nèi)核線程,它位于用戶層和系統(tǒng)層之間。系統(tǒng)對線程資源的分配、對線程的控制是通過輕進程來實現(xiàn)的,一個輕進程可以控制一個或多個線程。默認狀況下,啟動多少輕進程、哪些輕進程來控制哪些線程是由系統(tǒng)來控制的,這種狀況即稱為非綁定的。綁定狀況下,則顧名思義,即某個線程固定的”綁”在一個輕進程之上。被綁定的線程具有較高的響應(yīng)速度,這是因為CPU時間片的調(diào)度是面向輕進程的,綁定的線程可以保證在需要的時候它總有一個輕進程可用。通過設(shè)置被綁定的輕進程的優(yōu)先級和調(diào)度級可以使得綁定的線程滿足諸如實時反應(yīng)之類的要求。? 設(shè)置線程綁定狀態(tài)的函數(shù)為pthread_attr_setscope,它有兩個參數(shù),第一個是指向?qū)傩越Y(jié)構(gòu)的指針,第二個是綁定類型,它有兩個取值:PTHREAD_SCOPE_SYSTEM(綁定的)和PTHREAD_SCOPE_PROCESS(非綁定的)。下面的代碼即創(chuàng)建了一個綁定的線程。
#include pthread_attr_t attr;pthread_t tid;/*初始化屬性值,均設(shè)為默認值*/pthread_attr_init(&attr);pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);pthread_create(&tid, &attr, (void *) my_function, NULL);
1
2
3
4
5
6
7
8
9
線程的分離狀態(tài)決定一個線程以什么樣的方式來終止自己。在上面的例子中,我們采用了線程的默認屬性,即為非分離狀態(tài),這種情況下,原有的線程等待創(chuàng)建的線程結(jié)束。只有當pthread_join()函數(shù)返回時,創(chuàng)建的線程才算終止,才能釋放自己占用的系統(tǒng)資源。而分離線程不是這樣子的,它沒有被其他的線程所等待,自己運行結(jié)束了,線程也就終止了,馬上釋放系統(tǒng)資源。程序員應(yīng)該根據(jù)自己的需要,選擇適當?shù)姆蛛x狀態(tài)。設(shè)置線程分離狀態(tài)的函數(shù)為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個參數(shù)可選為PTHREAD_CREATE_DETACHED(分離線程)和 PTHREAD _CREATE_JOINABLE(非分離線程)。這里要注意的一點是,如果設(shè)置一個線程為分離線程,而這個線程運行又非常快,它很可能在pthread_create函數(shù)返回之前就終止了,它終止以后就可能將線程號和系統(tǒng)資源移交給其他的線程使用,這樣調(diào)用pthread_create的線程就得到了錯誤的線程號。要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創(chuàng)建的線程里調(diào)用pthread_cond_timewait函數(shù),讓這個線程等待一會兒,留出足夠的時間讓函數(shù)pthread_create返回。設(shè)置一段等待時間,是在多線程編程里常用的方法。但是注意不要使用諸如wait()之類的函數(shù),它們是使整個進程睡眠,并不能解決線程同步的問題。? 另外一個可能常用的屬性是線程的優(yōu)先級,它存放在結(jié)構(gòu)sched_param中。用函數(shù)pthread_attr_getschedparam和函數(shù)pthread_attr_setschedparam進行存放,一般說來,我們總是先取優(yōu)先級,對取得的值修改后再存放回去。下面即是一段簡單的例子。
#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);
1
2
3
4
5
6
7
8
9
10
11
12
4 線程的數(shù)據(jù)處理? 和進程相比,線程的最大優(yōu)點之一是數(shù)據(jù)的共享性,各個進程共享父進程處沿襲的數(shù)據(jù)段,可以方便的獲得、修改數(shù)據(jù)。但這也給多線程編程帶來了許多問題。我們必須當心有多個不同的進程訪問相同的變量。許多函數(shù)是不可重入的,即同時不能運行一個函數(shù)的多個拷貝(除非使用不同的數(shù)據(jù)段)。在函數(shù)中聲明的靜態(tài)變量常常帶來問題,函數(shù)的返回值也會有問題。因為如果返回的是函數(shù)內(nèi)部靜態(tài)聲明的空間的地址,則在一個線程調(diào)用該函數(shù)得到地址后使用該地址指向的數(shù)據(jù)時,別的線程可能調(diào)用此函數(shù)并修改了這一段數(shù)據(jù)。在進程中共享的變量必須用關(guān)鍵字volatile來定義,這是為了防止編譯器在優(yōu)化時(如gcc中使用-OX參數(shù))改變它們的使用方式。為了保護變量,我們必須使用信號量、互斥等方法來保證我們對變量的正確使用。下面,我們就逐步介紹處理線程數(shù)據(jù)時的有關(guān)知識。
4.1 線程數(shù)據(jù)? 在單線程的程序里,有兩種基本的數(shù)據(jù):全局變量和局部變量。但在多線程程序里,還有第三種數(shù)據(jù)類型:線程數(shù)據(jù)(TSD: Thread-Specific Data)。它和全局變量很象,在線程內(nèi)部,各個函數(shù)可以象使用全局變量一樣調(diào)用它,但它對線程外部的其它線程是不可見的。這種數(shù)據(jù)的必要性是顯而易見的。例如我們常見的變量errno,它返回標準的出錯信息。它顯然不能是一個局部變量,幾乎每個函數(shù)都應(yīng)該可以調(diào)用它;但它又不能是一個全局變量,否則在A線程里輸出的很可能是B線程的出錯信息。要實現(xiàn)諸如此類的變量,我們就必須使用線程數(shù)據(jù)。我們?yōu)槊總€線程數(shù)據(jù)創(chuàng)建一個鍵,它和這個鍵相關(guān)聯(lián),在各個線程里,都使用這個鍵來指代線程數(shù)據(jù),但在不同的線程里,這個鍵代表的數(shù)據(jù)是不同的,在同一個線程里,它代表同樣的數(shù)據(jù)內(nèi)容。? 和線程數(shù)據(jù)相關(guān)的函數(shù)主要有4個:創(chuàng)建一個鍵;為一個鍵指定線程數(shù)據(jù);從一個鍵讀取線程數(shù)據(jù);刪除鍵。? 創(chuàng)建鍵的函數(shù)原型為:? extern int pthread_key_create __P ((pthread_key_t *__key,? void (__destr_function) (void?)));? 第一個參數(shù)為指向一個鍵值的指針,第二個參數(shù)指明了一個destructor函數(shù),如果這個參數(shù)不為空,那么當每個線程結(jié)束時,系統(tǒng)將調(diào)用這個函數(shù)來釋放綁定在這個鍵上的內(nèi)存塊。這個函數(shù)常和函數(shù)pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被創(chuàng)建一次。函數(shù)pthread_once聲明一個初始化函數(shù),第一次調(diào)用pthread_once時它執(zhí)行這個函數(shù),以后的調(diào)用將被它忽略。
在下面的例子中,我們創(chuàng)建一個鍵,并將它和某個數(shù)據(jù)相關(guān)聯(lián)。我們要定義一個函數(shù)createWindow,這個函數(shù)定義一個圖形窗口(數(shù)據(jù)類型為Fl_Window *,這是圖形界面開發(fā)工具FLTK中的數(shù)據(jù)類型)。由于各個線程都會調(diào)用這個函數(shù),所以我們使用線程數(shù)據(jù)。?
/* 聲明一個鍵*/pthread_key_t myWinKey;/* 函數(shù) createWindow */void createWindow ( void ) {Fl_Window * win;static pthread_once_t once= PTHREAD_ONCE_INIT;/* 調(diào)用函數(shù)createMyKey,創(chuàng)建鍵*/pthread_once ( & once, createMyKey) ;/*win指向一個新建立的窗口*/win=new Fl_Window( 0, 0, 100, 100, "MyWindow");/* 對此窗口作一些可能的設(shè)置工作,如大小、位置、名稱等*/setWindow(win);/* 將窗口指針值綁定在鍵myWinKey上*/pthread_setpecific ( myWinKey, win);}/* 函數(shù) createMyKey,創(chuàng)建一個鍵,并指定了destructor */void createMyKey ( void ) {pthread_keycreate(&myWinKey, freeWinKey);}/* 函數(shù) freeWinKey,釋放空間*/void freeWinKey ( Fl_Window * win){delete win;}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
這樣,在不同的線程中調(diào)用函數(shù)createMyWin,都可以得到在線程內(nèi)部均可見的窗口變量,這個變量通過函數(shù)pthread_getspecific得到。在上面的例子中,我們已經(jīng)使用了函數(shù)pthread_setspecific來將線程數(shù)據(jù)和一個鍵綁定在一起。這兩個函數(shù)的原型如下:? extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));? extern void *pthread_getspecific __P ((pthread_key_t __key));? 這兩個函數(shù)的參數(shù)意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific為一個鍵指定新的線程數(shù)據(jù)時,必須自己釋放原有的線程數(shù)據(jù)以回收空間。這個過程函數(shù)pthread_key_delete用來刪除一個鍵,這個鍵占用的內(nèi)存將被釋放,但同樣要注意的是,它只釋放鍵占用的內(nèi)存,并不釋放該鍵關(guān)聯(lián)的線程數(shù)據(jù)所占用的內(nèi)存資源,而且它也不會觸發(fā)函數(shù)pthread_key_create中定義的destructor函數(shù)。線程數(shù)據(jù)的釋放必須在釋放鍵之前完成。
4.2 互斥鎖? 互斥鎖用來保證一段時間內(nèi)只有一個線程在執(zhí)行一段代碼。必要性顯而易見:假設(shè)各個線程向同一個文件順序?qū)懭霐?shù)據(jù),最后得到的結(jié)果一定是災(zāi)難性的。? 我們先看下面一段代碼。這是一個讀/寫程序,它們公用一個緩沖區(qū),并且我們假定一個緩沖區(qū)只能保存一條信息。即緩沖區(qū)只有兩個狀態(tà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; /* 定義延遲時間*/ delay.tv_sec = 2; delay.tv_nec = 0; /* 用默認屬性初始化一個互斥鎖對象*/ 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; } /* 打開互斥鎖*/ 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); }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
這里聲明了互斥鎖變量mutex,結(jié)構(gòu)pthread_mutex_t為不公開的數(shù)據(jù)類型,其中包含一個系統(tǒng)分配的屬性對象。函數(shù)pthread_mutex_init用來生成一個互斥鎖。NULL參數(shù)表明使用默認屬性。如果需要聲明特定屬性的互斥鎖,須調(diào)用函數(shù)pthread_mutexattr_init。函數(shù)pthread_mutexattr_setpshared和函數(shù)pthread_mutexattr_settype用來設(shè)置互斥鎖屬性。前一個函數(shù)設(shè)置屬性pshared,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來不同進程中的線程同步,后者用于同步本進程的不同線程。在上面的例子中,我們使用的是默認屬性PTHREAD_PROCESS_ PRIVATE。后者用來設(shè)置互斥鎖類型,可選的類型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上所、解鎖機制,一般情況下,選用最后一個默認屬性。? pthread_mutex_lock聲明開始用互斥鎖上鎖,此后的代碼直至調(diào)用pthread_mutex_unlock為止,均被上鎖,即同一時間只能被一個線程調(diào)用執(zhí)行。當一個線程執(zhí)行到pthread_mutex_lock處時,如果該鎖此時被另一個線程使用,那此線程被阻塞,即程序?qū)⒌却搅硪粋€線程釋放此互斥鎖。在上面的例子中,我們使用了pthread_delay_np函數(shù),讓線程睡眠一段時間,就是為了防止一個線程始終占據(jù)此函數(shù)。? 上面的例子非常簡單,就不再介紹了,需要提出的是在使用互斥鎖的過程中很有可能會出現(xiàn)死鎖:兩個線程試圖同時占用兩個資源,并按不同的次序鎖定相應(yīng)的互斥鎖,例如兩個線程都需要鎖定互斥鎖1和互斥鎖2,a線程先鎖定互斥鎖1,b線程先鎖定互斥鎖2,這時就出現(xiàn)了死鎖。此時我們可以使用函數(shù)pthread_mutex_trylock,它是函數(shù)pthread_mutex_lock的非阻塞版本,當它發(fā)現(xiàn)死鎖不可避免時,它會返回相應(yīng)的信息,程序員可以針對死鎖做出相應(yīng)的處理。另外不同的互斥鎖類型對死鎖的處理不一樣,但最主要的還是要程序員自己在程序設(shè)計注意這一點。
4.3 條件變量? 前一節(jié)中我們講述了如何使用互斥鎖來實現(xiàn)線程間數(shù)據(jù)的共享和通信,互斥鎖一個明顯的缺點是它只有兩種狀態(tài):鎖定和非鎖定。而條件變量通過允許線程阻塞和等待另一個線程發(fā)送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應(yīng)的互斥鎖并等待條件發(fā)生變化。一旦其它的某個線程改變了條件變量,它將通知相應(yīng)的條件變量喚醒一個或多個正被此條件變量阻塞的線程。這些線程將重新鎖定互斥鎖并重新測試條件是否滿足。一般說來,條件變量被用來進行線承間的同步。? 條件變量的結(jié)構(gòu)為pthread_cond_t,函數(shù)pthread_cond_init()被用來初始化一個條件變量。它的原型為:? extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));? 其中cond是一個指向結(jié)構(gòu)pthread_cond_t的指針,cond_attr是一個指向結(jié)構(gòu)pthread_condattr_t的指針。結(jié)構(gòu)pthread_condattr_t是條件變量的屬性結(jié)構(gòu),和互斥鎖一樣我們可以用它來設(shè)置條件變量是進程內(nèi)可用還是進程間可用,默認值是PTHREAD_ PROCESS_PRIVATE,即此條件變量被同一進程內(nèi)的各個線程使用。注意初始化條件變量只有未被使用時才能重新初始化或被釋放。釋放一個條件變量的函數(shù)為pthread_cond_ destroy(pthread_cond_t cond)。 ? 函數(shù)pthread_cond_wait()使線程阻塞在一個條件變量上。它的函數(shù)原型為:? extern int pthread_cond_wait __P ((pthread_cond_t *__cond,? pthread_mutex_t *__mutex));? 線程解開mutex指向的鎖并被條件變量cond阻塞。線程可以被函數(shù)pthread_cond_signal和函數(shù)pthread_cond_broadcast喚醒,但是要注意的是,條件變量只是起阻塞和喚醒線程的作用,具體的判斷條件還需用戶給出,例如一個變量是否為0等等,這一點我們從后面的例子中可以看到。線程被喚醒后,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來線程應(yīng)該仍阻塞在這里,被等待被下一次喚醒。這個過程一般用while語句實現(xiàn)。? 另一個用來阻塞線程的函數(shù)是pthread_cond_timedwait(),它的原型為:? extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,? pthread_mutex_t *__mutex, __const struct timespec *__abstime));? 它比函數(shù)pthread_cond_wait()多了一個時間參數(shù),經(jīng)歷abstime段時間后,即使條件變量不滿足,阻塞也被解除。? 函數(shù)pthread_cond_signal()的原型為:? extern int pthread_cond_signal __P ((pthread_cond_t *__cond));? 它用來釋放被阻塞在條件變量cond上的一個線程。多個線程阻塞在此條件變量上時,哪一個線程被喚醒是由線程的調(diào)度策略所決定的。要注意的是,必須用保護條件變量的互斥鎖來保護這個函數(shù),否則條件滿足信號又可能在測試條件和調(diào)用pthread_cond_wait函數(shù)之間被發(fā)出,從而造成無限制的等待。下面是使用函數(shù)pthread_cond_wait()和函數(shù)pthread_cond_signal()的一個簡單的例子。?
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);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
count值為0時,decrement函數(shù)在pthread_cond_wait處被阻塞,并打開互斥鎖count_lock。此時,當調(diào)用到函數(shù)increment_count時,pthread_cond_signal()函數(shù)改變條件變量,告知decrement_count()停止阻塞。讀者可以試著讓兩個線程分別運行這兩個函數(shù),看看會出現(xiàn)什么樣的結(jié)果。? 函數(shù)pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變量cond上的線程。這些線程被喚醒后將再次競爭相應(yīng)的互斥鎖,所以必須小心使用這個函數(shù)。
4.4 信號量? 信號量本質(zhì)上是一個非負的整數(shù)計數(shù)器,它被用來控制對公共資源的訪問。當公共資源增加時,調(diào)用函數(shù)sem_post()增加信號量。只有當信號量值大于0時,才能使用公共資源,使用后,函數(shù)sem_wait()減少信號量。函數(shù)sem_trywait()和函數(shù)pthread_ mutex_trylock()起同樣的作用,它是函數(shù)sem_wait()的非阻塞版本。下面我們逐個介紹和信號量有關(guān)的一些函數(shù),它們都在頭文件/usr/include/semaphore.h中定義。? 信號量的數(shù)據(jù)類型為結(jié)構(gòu)sem_t,它本質(zhì)上是一個長整型的數(shù)。函數(shù)sem_init()用來初始化一個信號量。它的原型為:? extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));? sem為指向信號量結(jié)構(gòu)的一個指針;pshared不為0時此信號量在進程間共享,否則只能為當前進程的所有線程共享;value給出了信號量的初始值。? 函數(shù)sem_post( sem_t *sem )用來增加信號量的值。當有線程阻塞在這個信號量上時,調(diào)用這個函數(shù)會使其中的一個線程不在阻塞,選擇機制同樣是由線程的調(diào)度策略決定的。? 函數(shù)sem_wait( sem_t *sem )被用來阻塞當前線程直到信號量sem的值大于0,解除阻塞后將sem的值減一,表明公共資源經(jīng)使用后減少。函數(shù)sem_trywait ( sem_t *sem )是函數(shù)sem_wait()的非阻塞版本,它直接將信號量sem的值減一。? 函數(shù)sem_destroy(sem_t *sem)用來釋放信號量sem。? 下面我們來看一個使用信號量的例子。在這個例子中,一共有4個線程,其中兩個線程負責(zé)從文件讀取數(shù)據(jù)到公共的緩沖區(qū),另兩個線程從緩沖區(qū)讀取數(shù)據(jù)作不同的處理(加和乘運算)。?
/* File sem.c */ #include < stdio.h > #include < pthread.h > #include < semaphore.h > #define MAXSTACK 100int stack[MAXSTACK][2];int size = 0;sem_t sem;/* 從文件1.dat讀取數(shù)據(jù),每讀一次,信號量加一*/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讀取數(shù)據(jù)*/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);}/*阻塞等待緩沖區(qū)有數(shù)據(jù),讀取數(shù)據(jù)后,釋放空間,繼續(xù)等待*/void HandleData1(void) { while (1) { sem_wait( & sem); printf("Plus:%d+%d=%d ", 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=%d ", 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); /* 防止程序過早退出,讓它在此無限期等待*/ pthread_join(t1, NULL);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
在Linux下,我們用命令gcc -lpthread sem.c -o sem生成可執(zhí)行文件sem。 我們事先編輯好數(shù)據(jù)文件1.dat和2.dat,假設(shè)它們的內(nèi)容分別為1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我們運行sem,得到如下的結(jié)果:? 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
從中我們可以看出各個線程間的競爭關(guān)系。而數(shù)值并未按我們原先的順序顯示出來這是由于size這個數(shù)值被各個線程任意修改的緣故。這也往往是多線程編程要注意的問題。
5 小結(jié)? 多線程編程是一個很有意思也很有用的技術(shù),使用多線程技術(shù)的網(wǎng)絡(luò)螞蟻是目前最常用的下載工具之一,使用多線程技術(shù)的grep比單線程的grep要快上幾倍,類似的例子還有很多。希望大家能用多線程技術(shù)寫出高效實用的好程序來。? 6、信號(signal)是一種軟件中斷,它提供了一種處理異步事件的方法,也是進程間惟一的異步通信方式。在Linux系統(tǒng)中,根據(jù)POSIX標準擴展以后的信號機制,不僅可以用來通知某種程序發(fā)生了什么事件,還可以給進程傳遞數(shù)據(jù)。? 6.1、信號的來源? 信號的來源可以有很多種試,按照產(chǎn)生條件的不同可以分為硬件和軟件兩種。? 6.1.1、 硬件方式? 當用戶在終端上按下某鍵時,將產(chǎn)生信號。如按下
#include void (*signal(int signum, void (*handler))(int)))(int);
1
2
如果該函數(shù)原型不容易理解的話,可以參考下面的分解方式來理解:? typedef void (*sighandler_t)(int);? sighandler_t signal(int signum, sighandler_t handler));? 第一個參數(shù)指定信號的值,第二個參數(shù)指定針對前面信號值的處理,可以忽略該信號(參數(shù)設(shè)為SIG_IGN);可以采用系統(tǒng)默認方式處理信號(參數(shù)設(shè)為SIG_DFL);也可以自己實現(xiàn)處理方式(參數(shù)指定一個函數(shù)地址)。? 如果signal()調(diào)用成功,返回最后一次為安裝信號signum而調(diào)用signal()時的handler值;失敗則返回SIG_ERR。
(2)、sigaction()
#include int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
1
2
sigaction函數(shù)用于改變進程接收到特定信號后的行為。該函數(shù)的第一個參數(shù)為信號的值,可以為除SIGKILL及SIGSTOP外的任何一個特定有效的信號(為這兩個信號定義自己的處理函數(shù),將導(dǎo)致信號安裝錯誤)。第二個參數(shù)是指向結(jié)構(gòu)sigaction的一個實例的指針,在結(jié)構(gòu)sigaction的實例中,指定了對特定信號的處理,可以為空,進程會以缺省方式對信號處理;第三個參數(shù)oldact指向的對象用來保存原來對相應(yīng)信號的處理,可指定oldact為NULL。如果把第二、第三個參數(shù)都設(shè)為NULL,那么該函數(shù)可用于檢查信號的有效性。
6.5.2、發(fā)送信號函數(shù)? (1) int raise(int sig); 對當前進程發(fā)送指定信號? (2) int pause(void); 將進程掛起等待信號? (3) int kill(pid_t pid,int sig); 通過進程編號發(fā)送信號? (4) unsigned int alarm(unsigned int seconds); 指定時間(秒)發(fā)送SIGALRM信號。 seconds 為0 時取消所有已設(shè)置的alarm請求;? (5)int sigqueue(pid_t pid,int sig,const union sigval val);類似于kill函數(shù),多了附帶共用體 union sigval形數(shù),將共用體中的成員 int sival_int 或 void *sival_ptr 的值傳遞給 信號處理函數(shù)中的定義類型 siginfo_t 中的 int si_int 或 void *si_ptr;? (6)int setitimer(int which,const struct itimerval *value,struct itimerval *oldvalue); 可定時發(fā)送信號,根據(jù)which可指定三種信號類型:SIGALRM、SIGVTALRM 和 SIGPROF;作用時間也因which值不同而不同;struct itimerval 的成員 it_interval定義間隔時間,it_value 為0時,使計時器失效;? (7) void abort(void) 將造成進程終止;除非捕獲SIGABORT信號;
6.5.3、信號集及信號集操作? sigfillset(sigset_t *set); 設(shè)置所有的信號到set信號集中;? sigemptyset(sigset_t *set); 從set信號集中清空所有信號;? sigaddset(sigset_t *set,int sig);在set信號集中加入sig信號;? sigdelset(sigset_t *set,int sig);在set信號集中刪除sig信號;? 6.5.4、阻塞信號相關(guān)函數(shù)? int sigprocmask(int how,const sigset_t *set,sigset_t *set); 根據(jù)how值,設(shè)置阻塞信號集,或釋放阻塞的信號集? int sigpending(sigset_t *set); 獲取在阻塞中的所有信號;? int sigsuspend(const sigset_t *set); 類似于 pause()函數(shù)!
?
評論
查看更多