Linux下的多執行緒程式設計(轉)
Linux下的多執行緒程式設計(轉)[@more@]1 引言
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到作業系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支援執行緒的概念,但是在一個程式(process)中只允許有一個執行緒,這樣多執行緒就意味著多程式。現在,多執行緒技術已經被許多作業系統所支援,包括Windows/NT,當然,也包括Linux。
使用多執行緒的理由之一是和程式相比,它是一種非常"節儉"的多工操作方式。例如,在Linux系統下,啟動一個新的程式必須分配給它獨立的地址空間,建立眾多的資料表來維護它的程式碼段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而執行於一個程式中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程式所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程式間切換所需要的時間。 使用多執行緒的理由之二是執行緒間方便的通訊機制。對不同程式來說,它們具有獨立的資料空間,要進行資料的傳遞只能透過通訊的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一程式下的執行緒之間共享資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共享也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的子程式中宣告為static的資料更有可能給多執行緒程式帶來災難性的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和程式比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式響應。這對圖形介面的程式尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程式不會響應鍵盤、滑鼠、選單的操作,而使用多執行緒技術,將耗時長的操作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。作業系統會保證當執行緒數不大於CPU數目時,不同的執行緒執行於不同的CPU上。
3) 改善程式結構。一個既長又複雜的程式可以考慮分為多個執行緒,成為幾個獨立或半獨立的執行部分,這樣的程式會利於理解和修改。
下面先編寫一個簡單的多執行緒程式。
2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。編寫Linux下的多執行緒程式,需要使用標頭檔案pthread.h,連線時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是透過系統呼叫clone()來實現的。clone()是Linux所特有的系統呼叫,它的使用方式類似fork。[table=95%][tr][td]/* 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);
}
[/td][/tr][/table]編譯此程式:
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.
前後兩次結果不一樣,這是兩個執行緒爭奪CPU資源的結果。上面的示例使用到了兩個函式,pthread_create和pthread_join,並宣告瞭一個pthread_t型的變數。
pthread_t在標頭檔案/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別符號。函式pthread_create用來建立一個執行緒,它的原型為:[table=95%][tr][td]extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,void *(*__start_routine) (void *), void *__arg));
[/td][/tr][/table] 第一個引數為指向執行緒識別符號的指標,第二個引數用來設定執行緒屬性,第三個引數是執行緒執行函式的起始地址,最後一個引數是執行函式的引數。這裡,我們的函式thread不需要引數,所以最後一個引數設為空指標。第二個引數我們也設為空指標,這樣將生成預設屬性的執行緒。對執行緒屬性的設定和修改我們將在下一節闡述。當建立執行緒成功時,函式返回0,若不為0則說明建立執行緒失敗,常見的錯誤返回程式碼為EAGAIN和EINVAL。前者表示系統限制建立新的執行緒,例如執行緒數目過多了;後者表示第二個引數代表的執行緒屬性值非法。建立執行緒成功後,新建立的執行緒則執行引數三和引數四確定的函式,原來的執行緒則繼續執行下一行程式碼。
函式pthread_join用來等待一個執行緒的結束。函式原型為:[table=95%][tr][td]extern int pthread_join __P ((pthread_t __th, void **__thread_return));
[/td][/tr][/table] 第一個引數為被等待的執行緒識別符號,第二個引數為一個使用者定義的指標,它可以用來儲存被等待執行緒的返回值。這個函式是一個執行緒阻塞的函式,呼叫它的函式將一直等待到被等待的執行緒結束為止,當函式返回時,被等待執行緒的資源被收回。一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函式結束了,呼叫它的執行緒也就結束了;另一種方式是透過函式pthread_exit來實現。它的函式原型為:[table=95%][tr][td]extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
[/td][/tr][/table] 唯一的引數是函式的返回程式碼,只要pthread_join中的第二個引數thread_return不是NULL,這個值將被傳遞給thread_return。最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到訊號的執行緒成功返回,其餘呼叫pthread_join的執行緒則返回錯誤程式碼ESRCH。
下面來了解執行緒的一些常用屬性以及如何設定這些屬性。
3 修改執行緒的屬性
上一節的例子用pthread_create函式建立了一個執行緒,在這個執行緒中使用了預設引數,即將該函式的第二個引數設為NULL。對大多數程式來說,使用預設屬性就夠了,但有必要來了解一下執行緒的有關屬性。
屬性結構為pthread_attr_t,它同樣在標頭檔案/usr/include/pthread.h中定義。屬性值不能直接設定,須使用相關函式進行操作,初始化的函式為pthread_attr_init,這個函式必須在pthread_create函式之前呼叫。屬性物件主要包括是否繫結、是否分離、堆疊地址、堆疊大小、優先順序。預設的屬性為非繫結、非分離、預設1M的堆疊、與父程式同樣級別的優先順序。
關於執行緒的繫結,牽涉到另外一個概念:輕程式(LWP:Light Weight Process)。輕程式可以理解為核心執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是透過輕程式來實現的,一個輕程式可以控制一個或多個執行緒。預設狀況下,啟動多少輕程式、哪些輕程式來控制哪些執行緒是由系統來控制的,這種狀況即稱為非繫結的。繫結狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕程式之上。被繫結的執行緒具有較高的響應速度,這是因為CPU時間片的排程是面向輕程式的,繫結的執行緒可以保證在需要的時候它總有一個輕程式可用。透過設定被繫結的輕程式的優先順序和排程級可以使得繫結的執行緒滿足諸如實時反應之類的要求。
設定執行緒繫結狀態的函式為pthread_attr_setscope,它有兩個引數,第一個是指向屬性結構的指標,第二個是繫結型別,它有兩個取值:PTHREAD_SCOPE_SYSTEM(繫結的)和PTHREAD_SCOPE_PROCESS(非繫結的)。下面的程式碼即建立了一個繫結的執行緒。[table=95%][tr][td]#include <pthread.h>
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);
[/td][/tr][/table] 執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。上面的例子中採用了執行緒的預設屬性,即為非分離狀態,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()函式返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己執行結束了,執行緒也就終止了,馬上釋放系統資源。設定執行緒分離狀態的函式為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個引數可選為PTHREAD_CREATE_DETACHED(分離執行緒)和 PTHREAD _CREATE_JOINABLE(非分離執行緒)。這裡要注意的一點是,如果設定一個執行緒為分離執行緒,而這個執行緒執行又非常快,它很可能在pthread_create函式返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣呼叫pthread_create的執行緒就得到了錯誤的執行緒號。要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被建立的執行緒裡呼叫pthread_cond_timewait函式,讓這個執行緒等待一會兒,留出足夠的時間讓函式pthread_create返回。設定一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函式,它們是使整個程式睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬性是執行緒的優先順序,它存放在結構sched_param中。用函式pthread_attr_getschedparam和函式pthread_attr_setschedparam進行存放,一般說來,總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。[table=95%][tr][td]#include <pthread.h>
#include <sched.h>
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);
[/td][/tr][/table]
4 執行緒的資料處理
和程式相比,執行緒的最大優點之一是資料的共享性,各個程式共享父程式處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。必須當心有多個不同的程式訪問相同的變數。許多函式是不可重入的,即同時不能執行一個函式的多個複製(除非使用不同的資料段)。在函式中宣告的靜態變數常常帶來問題,函式的返回值也會有問題。因為如果返回的是函式內部靜態宣告的空間的地址,則在一個執行緒呼叫該函式得到地址後使用該地址指向的資料時,別的執行緒可能呼叫此函式並修改了這一段資料。在程式中共享的變數必須用關鍵字volatile來定義,這是為了防止編譯器在最佳化時(如gcc中使用-OX引數)改變它們的使用方式。為了保護變數,必須使用訊號量、互斥等方法來保證對變數的正確使用。
4.1 執行緒資料
在單執行緒的程式裡,有兩種基本的資料:全域性變數和區域性變數。但在多執行緒程式裡,還有第三種資料型別:執行緒資料(TSD: Thread-Specific Data)。它和全域性變數很象,線上程內部,各個函式可以象使用全域性變數一樣呼叫它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要性是顯而易見的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數,否則在A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料建立一個鍵,它和這個鍵相關聯,在各個執行緒裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函式主要有4個:建立一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
建立鍵的函式原型為:[table=95%][tr][td]extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
[/td][/tr][/table] 第一個引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。這個函式常和函式pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被建立一次。函式pthread_once宣告一個初始化函式,第一次呼叫pthread_once時它執行這個函式,以後的呼叫將被它忽略。
下面的例子建立一個鍵,並將它和某個資料相關聯。要定義一個函式createWindow,這個函式定義一個圖形視窗(資料型別為Fl_Window *,這是圖形介面開發工具FLTK中的資料型別)。由於各個執行緒都會呼叫這個函式,所以使用執行緒資料。[table=95%][tr][td]/* 宣告一個鍵*/
pthread_key_t myWinKey;
/* 函式 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 呼叫函式createMyKey,建立鍵*/
pthread_once ( & once, createMyKey) ;
/*win指向一個新建立的視窗*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 對此視窗作一些可能的設定工作,如大小、位置、名稱等*/
setWindow(win);
/* 將視窗指標值繫結在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函式 createMyKey,建立一個鍵,並指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函式 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}
[/td][/tr][/table] 這樣,在不同的執行緒中呼叫函式createMyWin,都可以得到線上程內部均可見的視窗變數,這個變數透過函式pthread_getspecific得到。在上面的例子中,我們已經使用了函式pthread_setspecific來將執行緒資料和一個鍵繫結在一起。這兩個函式的原型如下:[table=95%][tr][td]extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
[/td][/tr][/table]這兩個函式的引數意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific為一個鍵指定新的執行緒資料時,必須自己釋放原有的執行緒資料以回收空間。這個過程函式pthread_key_delete用來刪除一個鍵,這個鍵佔用的記憶體將被釋放,但同樣要注意的是,它只釋放鍵佔用的記憶體,並不釋放該鍵關聯的執行緒資料所佔用的記憶體資源,而且它也不會觸發函式pthread_key_create中定義的destructor函式。執行緒資料的釋放必須在釋放鍵之前完成。
4.2 互斥鎖
互斥鎖用來保證一段時間內只有一個執行緒在執行一段程式碼。必要性顯而易見:假設各個執行緒向同一個檔案順序寫入資料,最後得到的結果一定是災難性的。
先看下面一段程式碼。這是一個讀/寫程式,它們公用一個緩衝區,並且假定一個緩衝區只能儲存一條資訊。即緩衝區只有兩個狀態:有資訊或沒有資訊。[table=95%][tr][td]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);
}
}
[/td][/tr][/table] 這裡宣告瞭互斥鎖變數mutex,結構pthread_mutex_t為不公開的資料型別,其中包含一個系統分配的屬性物件。函式pthread_mutex_init用來生成一個互斥鎖。NULL參數列明使用預設屬性。如果需要宣告特定屬性的互斥鎖,須呼叫函式pthread_mutexattr_init。函式pthread_mutexattr_setpshared和函式pthread_mutexattr_settype用來設定互斥鎖屬性。前一個函式設定屬性pshared,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來不同程式中的執行緒同步,後者用於同步本程式的不同執行緒。上面的例子使用的是預設屬性PTHREAD_PROCESS_ PRIVATE。後者用來設定互斥鎖型別,可選的型別有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上所、解鎖機制,一般情況下,選用最後一個預設屬性。
pthread_mutex_lock宣告開始用互斥鎖上鎖,此後的程式碼直至呼叫pthread_mutex_unlock為止,均被上鎖,即同一時間只能被一個執行緒呼叫執行。當一個執行緒執行到pthread_mutex_lock處時,如果該鎖此時被另一個執行緒使用,那此執行緒被阻塞,即程式將等待到另一個執行緒釋放此互斥鎖。上面的例子使用了pthread_delay_np函式,讓執行緒睡眠一段時間,就是為了防止一個執行緒始終佔據此函式。
上面的例子非常簡單,就不再介紹了,需要提出的是在使用互斥鎖的過程中很有可能會出現死鎖:兩個執行緒試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個執行緒都需要鎖定互斥鎖1和互斥鎖2,a執行緒先鎖定互斥鎖1,b執行緒先鎖定互斥鎖2,這時就出現了死鎖。此時可以使用函式pthread_mutex_trylock,它是函式pthread_mutex_lock的非阻塞版本,當它發現死鎖不可避免時,它會返回相應的資訊,程式設計師可以針對死鎖做出相應的處理。另外不同的互斥鎖型別對死鎖的處理不一樣,但最主要的還是要程式設計師自己在程式設計注意這一點。
4.3 條件變數
前一節中我們講述瞭如何使用互斥鎖來實現執行緒間資料的共享和通訊,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變數透過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變數被用來進行線承間的同步。
條件變數的結構為pthread_cond_t,函式pthread_cond_init()被用來初始化一個條件變數。它的原型為:[table=95%][tr][td]extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
[/td][/tr][/table] 其中cond是一個指向結構pthread_cond_t的指標,cond_attr是一個指向結構pthread_condattr_t的指標。結構pthread_condattr_t是條件變數的屬性結構,和互斥鎖一樣我們可以用它來設定條件變數是程式內可用還是程式間可用,預設值是PTHREAD_ PROCESS_PRIVATE,即此條件變數被同一程式內的各個執行緒使用。注意初始化條件變數只有未被使用時才能重新初始化或被釋放。釋放一個條件變數的函式為pthread_cond_ destroy(pthread_cond_t cond)。
函式pthread_cond_wait()使執行緒阻塞在一個條件變數上。它的函式原型為:[table=95%][tr][td]extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
[/td][/tr][/table] 執行緒解開mutex指向的鎖並被條件變數cond阻塞。執行緒可以被函式pthread_cond_signal和函式pthread_cond_broadcast喚醒,但是要注意的是,條件變數只是起阻塞和喚醒執行緒的作用,具體的判斷條件還需使用者給出,例如一個變數是否為0等等,這一點我們從後面的例子中可以看到。執行緒被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來執行緒應該仍阻塞在這裡,被等待被下一次喚醒。這個過程一般用while語句實現。
另一個用來阻塞執行緒的函式是pthread_cond_timedwait(),它的原型為:[table=95%][tr][td] extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
[/td][/tr][/table] 它比函式pthread_cond_wait()多了一個時間引數,經歷abstime段時間後,即使條件變數不滿足,阻塞也被解除。
函式pthread_cond_signal()的原型為:[table=95%][tr][td] extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
[/td][/tr][/table] 它用來釋放被阻塞在條件變數cond上的一個執行緒。多個執行緒阻塞在此條件變數上時,哪一個執行緒被喚醒是由執行緒的排程策略所決定的。要注意的是,必須用保護條件變數的互斥鎖來保護這個函式,否則條件滿足訊號又可能在測試條件和呼叫pthread_cond_wait函式之間被髮出,從而造成無限制的等待。下面是使用函式pthread_cond_wait()和函式pthread_cond_signal()的一個簡單的例子。[table=95%][tr][td]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);
}
[/td][/tr][/table] count值為0時,decrement函式在pthread_cond_wait處被阻塞,並開啟互斥鎖count_lock。此時,當呼叫到函式increment_count時,pthread_cond_signal()函式改變條件變數,告知decrement_count()停止阻塞。讀者可以試著讓兩個執行緒分別執行這兩個函式,看看會出現什麼樣的結果。
函式pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變數cond上的執行緒。這些執行緒被喚醒後將再次競爭相應的互斥鎖,所以必須小心使用這個函式。
4.4 訊號量
訊號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,呼叫函式sem_post()增加訊號量。只有當訊號量值大於0時,才能使用公共資源,使用後,函式sem_wait()減少訊號量。函式sem_trywait()和函式pthread_ mutex_trylock()起同樣的作用,它是函式sem_wait()的非阻塞版本。下面介紹和訊號量有關的一些函式,它們都在標頭檔案/usr/include/semaphore.h中定義。
訊號量的資料型別為結構sem_t,它本質上是一個長整型的數。函式sem_init()用來初始化一個訊號量。它的原型為:[table=95%][tr][td]extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
[/td][/tr][/table] sem為指向訊號量結構的一個指標;pshared不為0時此訊號量在程式間共享,否則只能為當前程式的所有執行緒共享;value給出了訊號量的初始值。
函式sem_post( sem_t *sem )用來增加訊號量的值。當有執行緒阻塞在這個訊號量上時,呼叫這個函式會使其中的一個執行緒不在阻塞,選擇機制同樣是由執行緒的排程策略決定的。
函式sem_wait( sem_t *sem )被用來阻塞當前執行緒直到訊號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函式sem_trywait ( sem_t *sem )是函式sem_wait()的非阻塞版本,它直接將訊號量sem的值減一。
函式sem_destroy(sem_t *sem)用來釋放訊號量sem。
下面看一個使用訊號量的例子。在這個例子中,一共有4個執行緒,其中兩個執行緒負責從檔案讀取資料到公共的緩衝區,另兩個執行緒從緩衝區讀取資料作不同的處理(加和乘運算)。[table=95%][tr][td]/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#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=%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);
}
[/td][/tr][/table] 在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
從中以看出各個執行緒間的競爭關係。而數值並未按原先的順序顯示出來這是由於size這個數值被各個執行緒任意修改的緣故。這也往往是多執行緒程式設計要注意的問題。
執行緒(thread)技術早在60年代就被提出,但真正應用多執行緒到作業系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支援執行緒的概念,但是在一個程式(process)中只允許有一個執行緒,這樣多執行緒就意味著多程式。現在,多執行緒技術已經被許多作業系統所支援,包括Windows/NT,當然,也包括Linux。
使用多執行緒的理由之一是和程式相比,它是一種非常"節儉"的多工操作方式。例如,在Linux系統下,啟動一個新的程式必須分配給它獨立的地址空間,建立眾多的資料表來維護它的程式碼段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而執行於一個程式中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程式所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程式間切換所需要的時間。 使用多執行緒的理由之二是執行緒間方便的通訊機制。對不同程式來說,它們具有獨立的資料空間,要進行資料的傳遞只能透過通訊的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一程式下的執行緒之間共享資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共享也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的子程式中宣告為static的資料更有可能給多執行緒程式帶來災難性的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和程式比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
1) 提高應用程式響應。這對圖形介面的程式尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程式不會響應鍵盤、滑鼠、選單的操作,而使用多執行緒技術,將耗時長的操作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。作業系統會保證當執行緒數不大於CPU數目時,不同的執行緒執行於不同的CPU上。
3) 改善程式結構。一個既長又複雜的程式可以考慮分為多個執行緒,成為幾個獨立或半獨立的執行部分,這樣的程式會利於理解和修改。
下面先編寫一個簡單的多執行緒程式。
2 簡單的多執行緒程式設計
Linux系統下的多執行緒遵循POSIX執行緒介面,稱為pthread。編寫Linux下的多執行緒程式,需要使用標頭檔案pthread.h,連線時需要使用庫libpthread.a。順便說一下,Linux下pthread的實現是透過系統呼叫clone()來實現的。clone()是Linux所特有的系統呼叫,它的使用方式類似fork。[table=95%][tr][td]/* 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);
}
[/td][/tr][/table]編譯此程式:
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.
前後兩次結果不一樣,這是兩個執行緒爭奪CPU資源的結果。上面的示例使用到了兩個函式,pthread_create和pthread_join,並宣告瞭一個pthread_t型的變數。
pthread_t在標頭檔案/usr/include/bits/pthreadtypes.h中定義:
typedef unsigned long int pthread_t;
它是一個執行緒的識別符號。函式pthread_create用來建立一個執行緒,它的原型為:[table=95%][tr][td]extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,void *(*__start_routine) (void *), void *__arg));
[/td][/tr][/table] 第一個引數為指向執行緒識別符號的指標,第二個引數用來設定執行緒屬性,第三個引數是執行緒執行函式的起始地址,最後一個引數是執行函式的引數。這裡,我們的函式thread不需要引數,所以最後一個引數設為空指標。第二個引數我們也設為空指標,這樣將生成預設屬性的執行緒。對執行緒屬性的設定和修改我們將在下一節闡述。當建立執行緒成功時,函式返回0,若不為0則說明建立執行緒失敗,常見的錯誤返回程式碼為EAGAIN和EINVAL。前者表示系統限制建立新的執行緒,例如執行緒數目過多了;後者表示第二個引數代表的執行緒屬性值非法。建立執行緒成功後,新建立的執行緒則執行引數三和引數四確定的函式,原來的執行緒則繼續執行下一行程式碼。
函式pthread_join用來等待一個執行緒的結束。函式原型為:[table=95%][tr][td]extern int pthread_join __P ((pthread_t __th, void **__thread_return));
[/td][/tr][/table] 第一個引數為被等待的執行緒識別符號,第二個引數為一個使用者定義的指標,它可以用來儲存被等待執行緒的返回值。這個函式是一個執行緒阻塞的函式,呼叫它的函式將一直等待到被等待的執行緒結束為止,當函式返回時,被等待執行緒的資源被收回。一個執行緒的結束有兩種途徑,一種是象我們上面的例子一樣,函式結束了,呼叫它的執行緒也就結束了;另一種方式是透過函式pthread_exit來實現。它的函式原型為:[table=95%][tr][td]extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
[/td][/tr][/table] 唯一的引數是函式的返回程式碼,只要pthread_join中的第二個引數thread_return不是NULL,這個值將被傳遞給thread_return。最後要說明的是,一個執行緒不能被多個執行緒等待,否則第一個接收到訊號的執行緒成功返回,其餘呼叫pthread_join的執行緒則返回錯誤程式碼ESRCH。
下面來了解執行緒的一些常用屬性以及如何設定這些屬性。
3 修改執行緒的屬性
上一節的例子用pthread_create函式建立了一個執行緒,在這個執行緒中使用了預設引數,即將該函式的第二個引數設為NULL。對大多數程式來說,使用預設屬性就夠了,但有必要來了解一下執行緒的有關屬性。
屬性結構為pthread_attr_t,它同樣在標頭檔案/usr/include/pthread.h中定義。屬性值不能直接設定,須使用相關函式進行操作,初始化的函式為pthread_attr_init,這個函式必須在pthread_create函式之前呼叫。屬性物件主要包括是否繫結、是否分離、堆疊地址、堆疊大小、優先順序。預設的屬性為非繫結、非分離、預設1M的堆疊、與父程式同樣級別的優先順序。
關於執行緒的繫結,牽涉到另外一個概念:輕程式(LWP:Light Weight Process)。輕程式可以理解為核心執行緒,它位於使用者層和系統層之間。系統對執行緒資源的分配、對執行緒的控制是透過輕程式來實現的,一個輕程式可以控制一個或多個執行緒。預設狀況下,啟動多少輕程式、哪些輕程式來控制哪些執行緒是由系統來控制的,這種狀況即稱為非繫結的。繫結狀況下,則顧名思義,即某個執行緒固定的"綁"在一個輕程式之上。被繫結的執行緒具有較高的響應速度,這是因為CPU時間片的排程是面向輕程式的,繫結的執行緒可以保證在需要的時候它總有一個輕程式可用。透過設定被繫結的輕程式的優先順序和排程級可以使得繫結的執行緒滿足諸如實時反應之類的要求。
設定執行緒繫結狀態的函式為pthread_attr_setscope,它有兩個引數,第一個是指向屬性結構的指標,第二個是繫結型別,它有兩個取值:PTHREAD_SCOPE_SYSTEM(繫結的)和PTHREAD_SCOPE_PROCESS(非繫結的)。下面的程式碼即建立了一個繫結的執行緒。[table=95%][tr][td]#include <pthread.h>
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);
[/td][/tr][/table] 執行緒的分離狀態決定一個執行緒以什麼樣的方式來終止自己。上面的例子中採用了執行緒的預設屬性,即為非分離狀態,這種情況下,原有的執行緒等待建立的執行緒結束。只有當pthread_join()函式返回時,建立的執行緒才算終止,才能釋放自己佔用的系統資源。而分離執行緒不是這樣子的,它沒有被其他的執行緒所等待,自己執行結束了,執行緒也就終止了,馬上釋放系統資源。設定執行緒分離狀態的函式為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個引數可選為PTHREAD_CREATE_DETACHED(分離執行緒)和 PTHREAD _CREATE_JOINABLE(非分離執行緒)。這裡要注意的一點是,如果設定一個執行緒為分離執行緒,而這個執行緒執行又非常快,它很可能在pthread_create函式返回之前就終止了,它終止以後就可能將執行緒號和系統資源移交給其他的執行緒使用,這樣呼叫pthread_create的執行緒就得到了錯誤的執行緒號。要避免這種情況可以採取一定的同步措施,最簡單的方法之一是可以在被建立的執行緒裡呼叫pthread_cond_timewait函式,讓這個執行緒等待一會兒,留出足夠的時間讓函式pthread_create返回。設定一段等待時間,是在多執行緒程式設計裡常用的方法。但是注意不要使用諸如wait()之類的函式,它們是使整個程式睡眠,並不能解決執行緒同步的問題。
另外一個可能常用的屬性是執行緒的優先順序,它存放在結構sched_param中。用函式pthread_attr_getschedparam和函式pthread_attr_setschedparam進行存放,一般說來,總是先取優先順序,對取得的值修改後再存放回去。下面即是一段簡單的例子。[table=95%][tr][td]#include <pthread.h>
#include <sched.h>
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);
[/td][/tr][/table]
4 執行緒的資料處理
和程式相比,執行緒的最大優點之一是資料的共享性,各個程式共享父程式處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。必須當心有多個不同的程式訪問相同的變數。許多函式是不可重入的,即同時不能執行一個函式的多個複製(除非使用不同的資料段)。在函式中宣告的靜態變數常常帶來問題,函式的返回值也會有問題。因為如果返回的是函式內部靜態宣告的空間的地址,則在一個執行緒呼叫該函式得到地址後使用該地址指向的資料時,別的執行緒可能呼叫此函式並修改了這一段資料。在程式中共享的變數必須用關鍵字volatile來定義,這是為了防止編譯器在最佳化時(如gcc中使用-OX引數)改變它們的使用方式。為了保護變數,必須使用訊號量、互斥等方法來保證對變數的正確使用。
4.1 執行緒資料
在單執行緒的程式裡,有兩種基本的資料:全域性變數和區域性變數。但在多執行緒程式裡,還有第三種資料型別:執行緒資料(TSD: Thread-Specific Data)。它和全域性變數很象,線上程內部,各個函式可以象使用全域性變數一樣呼叫它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要性是顯而易見的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數,否則在A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料建立一個鍵,它和這個鍵相關聯,在各個執行緒裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。
和執行緒資料相關的函式主要有4個:建立一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。
建立鍵的函式原型為:[table=95%][tr][td]extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
[/td][/tr][/table] 第一個引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。這個函式常和函式pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被建立一次。函式pthread_once宣告一個初始化函式,第一次呼叫pthread_once時它執行這個函式,以後的呼叫將被它忽略。
下面的例子建立一個鍵,並將它和某個資料相關聯。要定義一個函式createWindow,這個函式定義一個圖形視窗(資料型別為Fl_Window *,這是圖形介面開發工具FLTK中的資料型別)。由於各個執行緒都會呼叫這個函式,所以使用執行緒資料。[table=95%][tr][td]/* 宣告一個鍵*/
pthread_key_t myWinKey;
/* 函式 createWindow */
void createWindow ( void ) {
Fl_Window * win;
static pthread_once_t once= PTHREAD_ONCE_INIT;
/* 呼叫函式createMyKey,建立鍵*/
pthread_once ( & once, createMyKey) ;
/*win指向一個新建立的視窗*/
win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
/* 對此視窗作一些可能的設定工作,如大小、位置、名稱等*/
setWindow(win);
/* 將視窗指標值繫結在鍵myWinKey上*/
pthread_setpecific ( myWinKey, win);
}
/* 函式 createMyKey,建立一個鍵,並指定了destructor */
void createMyKey ( void ) {
pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函式 freeWinKey,釋放空間*/
void freeWinKey ( Fl_Window * win){
delete win;
}
[/td][/tr][/table] 這樣,在不同的執行緒中呼叫函式createMyWin,都可以得到線上程內部均可見的視窗變數,這個變數透過函式pthread_getspecific得到。在上面的例子中,我們已經使用了函式pthread_setspecific來將執行緒資料和一個鍵繫結在一起。這兩個函式的原型如下:[table=95%][tr][td]extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
[/td][/tr][/table]這兩個函式的引數意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific為一個鍵指定新的執行緒資料時,必須自己釋放原有的執行緒資料以回收空間。這個過程函式pthread_key_delete用來刪除一個鍵,這個鍵佔用的記憶體將被釋放,但同樣要注意的是,它只釋放鍵佔用的記憶體,並不釋放該鍵關聯的執行緒資料所佔用的記憶體資源,而且它也不會觸發函式pthread_key_create中定義的destructor函式。執行緒資料的釋放必須在釋放鍵之前完成。
4.2 互斥鎖
互斥鎖用來保證一段時間內只有一個執行緒在執行一段程式碼。必要性顯而易見:假設各個執行緒向同一個檔案順序寫入資料,最後得到的結果一定是災難性的。
先看下面一段程式碼。這是一個讀/寫程式,它們公用一個緩衝區,並且假定一個緩衝區只能儲存一條資訊。即緩衝區只有兩個狀態:有資訊或沒有資訊。[table=95%][tr][td]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);
}
}
[/td][/tr][/table] 這裡宣告瞭互斥鎖變數mutex,結構pthread_mutex_t為不公開的資料型別,其中包含一個系統分配的屬性物件。函式pthread_mutex_init用來生成一個互斥鎖。NULL參數列明使用預設屬性。如果需要宣告特定屬性的互斥鎖,須呼叫函式pthread_mutexattr_init。函式pthread_mutexattr_setpshared和函式pthread_mutexattr_settype用來設定互斥鎖屬性。前一個函式設定屬性pshared,它有兩個取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用來不同程式中的執行緒同步,後者用於同步本程式的不同執行緒。上面的例子使用的是預設屬性PTHREAD_PROCESS_ PRIVATE。後者用來設定互斥鎖型別,可選的型別有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它們分別定義了不同的上所、解鎖機制,一般情況下,選用最後一個預設屬性。
pthread_mutex_lock宣告開始用互斥鎖上鎖,此後的程式碼直至呼叫pthread_mutex_unlock為止,均被上鎖,即同一時間只能被一個執行緒呼叫執行。當一個執行緒執行到pthread_mutex_lock處時,如果該鎖此時被另一個執行緒使用,那此執行緒被阻塞,即程式將等待到另一個執行緒釋放此互斥鎖。上面的例子使用了pthread_delay_np函式,讓執行緒睡眠一段時間,就是為了防止一個執行緒始終佔據此函式。
上面的例子非常簡單,就不再介紹了,需要提出的是在使用互斥鎖的過程中很有可能會出現死鎖:兩個執行緒試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個執行緒都需要鎖定互斥鎖1和互斥鎖2,a執行緒先鎖定互斥鎖1,b執行緒先鎖定互斥鎖2,這時就出現了死鎖。此時可以使用函式pthread_mutex_trylock,它是函式pthread_mutex_lock的非阻塞版本,當它發現死鎖不可避免時,它會返回相應的資訊,程式設計師可以針對死鎖做出相應的處理。另外不同的互斥鎖型別對死鎖的處理不一樣,但最主要的還是要程式設計師自己在程式設計注意這一點。
4.3 條件變數
前一節中我們講述瞭如何使用互斥鎖來實現執行緒間資料的共享和通訊,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變數透過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變數被用來進行線承間的同步。
條件變數的結構為pthread_cond_t,函式pthread_cond_init()被用來初始化一個條件變數。它的原型為:[table=95%][tr][td]extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
[/td][/tr][/table] 其中cond是一個指向結構pthread_cond_t的指標,cond_attr是一個指向結構pthread_condattr_t的指標。結構pthread_condattr_t是條件變數的屬性結構,和互斥鎖一樣我們可以用它來設定條件變數是程式內可用還是程式間可用,預設值是PTHREAD_ PROCESS_PRIVATE,即此條件變數被同一程式內的各個執行緒使用。注意初始化條件變數只有未被使用時才能重新初始化或被釋放。釋放一個條件變數的函式為pthread_cond_ destroy(pthread_cond_t cond)。
函式pthread_cond_wait()使執行緒阻塞在一個條件變數上。它的函式原型為:[table=95%][tr][td]extern int pthread_cond_wait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex));
[/td][/tr][/table] 執行緒解開mutex指向的鎖並被條件變數cond阻塞。執行緒可以被函式pthread_cond_signal和函式pthread_cond_broadcast喚醒,但是要注意的是,條件變數只是起阻塞和喚醒執行緒的作用,具體的判斷條件還需使用者給出,例如一個變數是否為0等等,這一點我們從後面的例子中可以看到。執行緒被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來執行緒應該仍阻塞在這裡,被等待被下一次喚醒。這個過程一般用while語句實現。
另一個用來阻塞執行緒的函式是pthread_cond_timedwait(),它的原型為:[table=95%][tr][td] extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond,
pthread_mutex_t *__mutex, __const struct timespec *__abstime));
[/td][/tr][/table] 它比函式pthread_cond_wait()多了一個時間引數,經歷abstime段時間後,即使條件變數不滿足,阻塞也被解除。
函式pthread_cond_signal()的原型為:[table=95%][tr][td] extern int pthread_cond_signal __P ((pthread_cond_t *__cond));
[/td][/tr][/table] 它用來釋放被阻塞在條件變數cond上的一個執行緒。多個執行緒阻塞在此條件變數上時,哪一個執行緒被喚醒是由執行緒的排程策略所決定的。要注意的是,必須用保護條件變數的互斥鎖來保護這個函式,否則條件滿足訊號又可能在測試條件和呼叫pthread_cond_wait函式之間被髮出,從而造成無限制的等待。下面是使用函式pthread_cond_wait()和函式pthread_cond_signal()的一個簡單的例子。[table=95%][tr][td]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);
}
[/td][/tr][/table] count值為0時,decrement函式在pthread_cond_wait處被阻塞,並開啟互斥鎖count_lock。此時,當呼叫到函式increment_count時,pthread_cond_signal()函式改變條件變數,告知decrement_count()停止阻塞。讀者可以試著讓兩個執行緒分別執行這兩個函式,看看會出現什麼樣的結果。
函式pthread_cond_broadcast(pthread_cond_t *cond)用來喚醒所有被阻塞在條件變數cond上的執行緒。這些執行緒被喚醒後將再次競爭相應的互斥鎖,所以必須小心使用這個函式。
4.4 訊號量
訊號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。當公共資源增加時,呼叫函式sem_post()增加訊號量。只有當訊號量值大於0時,才能使用公共資源,使用後,函式sem_wait()減少訊號量。函式sem_trywait()和函式pthread_ mutex_trylock()起同樣的作用,它是函式sem_wait()的非阻塞版本。下面介紹和訊號量有關的一些函式,它們都在標頭檔案/usr/include/semaphore.h中定義。
訊號量的資料型別為結構sem_t,它本質上是一個長整型的數。函式sem_init()用來初始化一個訊號量。它的原型為:[table=95%][tr][td]extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
[/td][/tr][/table] sem為指向訊號量結構的一個指標;pshared不為0時此訊號量在程式間共享,否則只能為當前程式的所有執行緒共享;value給出了訊號量的初始值。
函式sem_post( sem_t *sem )用來增加訊號量的值。當有執行緒阻塞在這個訊號量上時,呼叫這個函式會使其中的一個執行緒不在阻塞,選擇機制同樣是由執行緒的排程策略決定的。
函式sem_wait( sem_t *sem )被用來阻塞當前執行緒直到訊號量sem的值大於0,解除阻塞後將sem的值減一,表明公共資源經使用後減少。函式sem_trywait ( sem_t *sem )是函式sem_wait()的非阻塞版本,它直接將訊號量sem的值減一。
函式sem_destroy(sem_t *sem)用來釋放訊號量sem。
下面看一個使用訊號量的例子。在這個例子中,一共有4個執行緒,其中兩個執行緒負責從檔案讀取資料到公共的緩衝區,另兩個執行緒從緩衝區讀取資料作不同的處理(加和乘運算)。[table=95%][tr][td]/* File sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#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=%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);
}
[/td][/tr][/table] 在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
從中以看出各個執行緒間的競爭關係。而數值並未按原先的順序顯示出來這是由於size這個數值被各個執行緒任意修改的緣故。這也往往是多執行緒程式設計要注意的問題。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-960966/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Linux下的多執行緒程式設計Linux執行緒程式設計
- DELPHI下的多執行緒程式設計(1) (轉)執行緒程式設計
- DELPHI下的多執行緒程式設計(2) (轉)執行緒程式設計
- 多執行緒程式設計(轉)執行緒程式設計
- Linux系統下的多執行緒程式設計入門(轉)Linux執行緒程式設計
- linux 多執行緒程式設計Linux執行緒程式設計
- .NET多執行緒程式設計(1):多工和多執行緒 (轉)執行緒程式設計
- .NET多執行緒程式設計(3):執行緒同步 (轉)執行緒程式設計
- Linux下應用程式開發:QT中的多執行緒程式設計(轉)LinuxQT執行緒程式設計
- .NET多執行緒程式設計(4):執行緒池和非同步程式設計 (轉)執行緒程式設計非同步
- Linux 多執行緒程式設計(不限Linux)Linux執行緒程式設計
- 多執行緒程式設計執行緒程式設計
- Linux C++ 多執行緒程式設計LinuxC++執行緒程式設計
- iOS多執行緒程式設計總結(下)iOS執行緒程式設計
- Java多執行緒程式設計入門(轉)Java執行緒程式設計
- JavaScript多執行緒程式設計JavaScript執行緒程式設計
- Boost多執行緒程式設計執行緒程式設計
- UNIX多執行緒程式設計執行緒程式設計
- 程式設計思想之多執行緒與多程式(3):Java 中的多執行緒程式設計執行緒Java
- 多執行緒程式設計基礎(一)-- 執行緒的使用執行緒程式設計
- 多執行緒程式設計,處理多執行緒的併發問題(執行緒池)執行緒程式設計
- 深入淺出Java多執行緒程式設計(轉)Java執行緒程式設計
- 使用執行緒池優化多執行緒程式設計執行緒優化程式設計
- iOS多執行緒程式設計:執行緒同步總結iOS執行緒程式設計
- [短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全執行緒程式設計
- 程式設計思想之多執行緒與多程式(4):C++ 中的多執行緒程式設計執行緒C++
- Linux多執行緒伺服器端程式設計Linux執行緒伺服器程式設計
- Linux多執行緒程式設計———重點區分Linux執行緒程式設計
- 多執行緒程式設計基礎(二)-- 執行緒池的使用執行緒程式設計
- C#多執行緒程式設計(1):執行緒的啟動C#執行緒程式設計
- Linux下多執行緒程式設計與訊號處理易疏忽的一個例子(轉)Linux執行緒程式設計
- python 多執行緒程式設計Python執行緒程式設計
- Python多執行緒程式設計Python執行緒程式設計
- GCD 與多執行緒程式設計GC執行緒程式設計
- IOS多執行緒程式設計:概述iOS執行緒程式設計
- C#多執行緒程式設計C#執行緒程式設計
- 多執行緒程式設計ExecutorService用法執行緒程式設計
- 29. 多執行緒程式設計執行緒程式設計