1. 程式人生 > >C多執行緒程式設計以及執行緒函式

C多執行緒程式設計以及執行緒函式

執行緒的資料處理

  和程序相比,執行緒的最大優點之一是資料的共享性,各個程序共享父程序處沿襲的資料段,可以方便的獲得、修改資料。但這也給多執行緒程式設計帶來了許多問題。我們必須當心有多個不同的程序訪問相同的變數。許多函式是不可重入的,即同時不能執行一個函式的多個拷貝(除非使用不同的資料段)。在函式中宣告的靜態變數常常帶來問題,函式的返回值也會有問題。因為如果返回的是函式內部靜態宣告的空間的地址,則在一個執行緒呼叫該函式得到地址後使用該地址指向的資料時,別的執行緒可能呼叫此函式並修改了這一段資料。在程序中共享的變數必須用關鍵字volatile來定義,這是為了防止編譯器在優化時(如gcc中使用-OX引數)改變它們的使用方式。

為了保護變數,我們必須使用訊號量、互斥等方法來保證我們對變數的正確使用。下面,我們就逐步介紹處理執行緒資料時的有關知識。 

    4.1 執行緒資料

  在單執行緒的程式裡,有兩種基本的資料:全域性變數和區域性變數。但在多執行緒程式裡,還有第三種資料型別:執行緒資料(TSD: Thread-Specific Data)。它和全域性變數很象,線上程內部,各個函式可以象使用全域性變數一樣呼叫它,但它對執行緒外部的其它執行緒是不可見的。這種資料的必要性是顯而易見的。例如我們常見的變數errno,它返回標準的出錯資訊。它顯然不能是一個區域性變數,幾乎每個函式都應該可以呼叫它;但它又不能是一個全域性變數,否則在A執行緒裡輸出的很可能是B執行緒的出錯資訊。要實現諸如此類的變數,我們就必須使用執行緒資料。我們為每個執行緒資料建立一個鍵,它和這個鍵相關聯,在各個執行緒裡,都使用這個鍵來指代執行緒資料,但在不同的執行緒裡,這個鍵代表的資料是不同的,在同一個執行緒裡,它代表同樣的資料內容。

    和執行緒資料相關的函式主要有4個:建立一個鍵;為一個鍵指定執行緒資料;從一個鍵讀取執行緒資料;刪除鍵。 
    建立鍵的函式原型為: 
    extern int pthread_key_create __P ((pthread_key_t *__key, 
    void (*__destr_function) (void *)));

  第一個引數為指向一個鍵值的指標,第二個引數指明瞭一個destructor函式,如果這個引數不為空,那麼當每個執行緒結束時,系統將呼叫這個函式來釋放繫結在這個鍵上的記憶體塊。這個函式常和函式pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))一起使用,為了讓這個鍵只被建立一次。函式pthread_once宣告一個初始化函式,第一次呼叫pthread_once時它執行這個函式,以後的呼叫將被它忽略。 

  在下面的例子中,我們建立一個鍵,並將它和某個資料相關聯。

我們要定義一個函式createWindow,這個函式定義一個圖形視窗(資料型別為Fl_Window *,這是圖形介面開發工具FLTK中的資料型別)。由於各個執行緒都會呼叫這個函式,所以我們使用執行緒資料。

/* 宣告一個鍵*/ 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( 00100100"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; }   這樣,在不同的執行緒中呼叫函式createMyWin,都可以得到線上程內部均可見的視窗變數,這個變數通過函式pthread_getspecific得到。在上面的例子中,我們已經使用了函式pthread_setspecific來將執行緒資料和一個鍵繫結在一起。這兩個函式的原型如下: 
      extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer)); 
      extern void *pthread_getspecific __P ((pthread_key_t __key));   這兩個函式的引數意義和使用方法是顯而易見的。要注意的是,用pthread_setspecific為一個鍵指定新的執行緒資料時,必須自己釋放原有的執行緒資料以回收空間。這個過程函式pthread_key_delete用來刪除一個鍵,這個鍵佔用的記憶體將被釋放,但同樣要注意的是,它只釋放鍵佔用的記憶體,並不釋放該鍵關聯的執行緒資料所佔用的記憶體資源,而且它也不會觸發函式pthread_key_create中定義的destructor函式。執行緒資料的釋放必須在釋放鍵之前完成。 4.2 互斥鎖   互斥鎖用來保證一段時間內只有一個執行緒在執行一段程式碼。必要性顯而易見:假設各個執行緒向同一個檔案順序寫入資料,最後得到的結果一定是災難性的。   我們先看下面一段程式碼。這是一個讀/寫程式,它們公用一個緩衝區,並且我們假定一個緩衝區只能儲存一條資訊。即緩衝區只有兩個狀態:有資訊或沒有資訊。 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);   } }

  這裡聲明瞭互斥鎖變數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函式,讓執行緒睡眠一段時間,就是為了防止一個執行緒始終佔據此函式。(如果用sleep(2)則是讓程序睡眠了)

  上面的例子非常簡單,就不再介紹了,需要提出的是在使用互斥鎖的過程中很有可能會出現死鎖:兩個執行緒試圖同時佔用兩個資源,並按不同的次序鎖定相應的互斥鎖,例如兩個執行緒都需要鎖定互斥鎖1和互斥鎖2,a執行緒先鎖定互斥鎖1,b執行緒先鎖定互斥鎖2,這時就出現了死鎖。此時我們可以使用函式pthread_mutex_trylock,它是函式pthread_mutex_lock的非阻塞版本,當它發現死鎖不可避免時,它會返回相應的資訊,程式設計師可以針對死鎖做出相應的處理。另外不同的互斥鎖型別對死鎖的處理不一樣,但最主要的還是要程式設計師自己在程式設計注意這一點。

4.3 條件變數

  前一節中我們講述瞭如何使用互斥鎖來實現執行緒間資料的共享和通訊,互斥鎖一個明顯的缺點是它只有兩種狀態:鎖定和非鎖定。而條件變數通過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒。這些執行緒將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變數被用來進行線承間的同步。

    條件變數的結構為pthread_cond_t,函式pthread_cond_init()被用來初始化一個條件變數。它的原型為: 
    extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));

    其中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()使執行緒阻塞在一個條件變數上。它的函式原型為: 
    extern int pthread_cond_wait __P ((pthread_cond_t *__cond, 
    pthread_mutex_t *__mutex));

    執行緒解開mutex指向的鎖並被條件變數cond阻塞。執行緒可以被函式pthread_cond_signal和函式pthread_cond_broadcast喚醒,但是要注意的是,條件變數只是起阻塞和喚醒執行緒的作用,具體的判斷條件還需使用者給出,例如一個變數是否為0等等,這一點我們從後面的例子中可以看到。執行緒被喚醒後,它將重新檢查判斷條件是否滿足,如果還不滿足,一般說來執行緒應該仍阻塞在這裡,被等待被下一次喚醒。這個過程一般用while語句實現。

    另一個用來阻塞執行緒的函式是pthread_cond_timedwait(),它的原型為: 
    extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond, 
    pthread_mutex_t *__mutex, __const struct timespec *__abstime));

    它比函式pthread_cond_wait()多了一個時間引數,經歷abstime段時間後,即使條件變數不滿足,阻塞也被解除。 
    函式pthread_cond_signal()的原型為: 
    extern int pthread_cond_signal __P ((pthread_cond_t *__cond));

    它用來釋放被阻塞在條件變數cond上的一個執行緒。多個執行緒阻塞在此條件變數上時,哪一個執行緒被喚醒是由執行緒的排程策略所決定的。要注意的是,必須用保護條件變數的互斥鎖來保護這個函式,否則條件滿足訊號又可能在測試條件和呼叫pthread_cond_wait函式之間被髮出,從而造成無限制的等待。下面是使用函式pthread_cond_wait()和函式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); }   

  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()用來初始化一個訊號量。它的原型為: 

  extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));

  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個執行緒,其中兩個執行緒負責從檔案讀取資料到公共的緩衝區,另兩個執行緒從緩衝區讀取資料作不同的處理(加和乘運算)。

/* 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\n",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\n",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); }

  在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這個數值被各個執行緒任意修改的緣故。這也往往是多執行緒程式設計要注意的問題。 

 小結 

   多執行緒程式設計是一個很有意思也很有用的技術,使用多執行緒技術的網路螞蟻是目前最常用的下載工具之一,使用多執行緒技術的grep比單執行緒的grep要快上幾倍,類似的例子還有很多。希望大家能用多執行緒技術寫出高效實用的好程式來。