Linux系統程式設計(27)——執行緒控制

尹成發表於2014-09-04

程式在各自獨立的地址空間中執行,程式之間共享資料需要用mmap或者程式間通訊機制,那麼如何在一個程式的地址空間中執行多個執行緒呢。有些情況需要在一個程式中同時執行多個控制流程,這時候執行緒就派上了用場,比如實現一個圖形介面的下載軟體,一方面需要和使用者互動,等待和處理使用者的滑鼠鍵盤事件,另一方面又需要同時下載多個檔案,等待和處理從多個網路主機發來的資料,這些任務都需要一個“等待-處理”的迴圈,可以用多執行緒實現,一個執行緒專門負責與使用者互動,另外幾個執行緒每個執行緒負責和一個網路主機通訊。

main函式和訊號處理函式是同一個程式地址空間中的多個控制流程,多執行緒也是如此,但是比訊號處理函式更加靈活,訊號處理函式的控制流程只是在訊號遞達時產生,在處理完訊號之後就結束,而多執行緒的控制流程可以長期並存,作業系統會在各執行緒之間排程和切換,就像在多個程式之間排程和切換一樣。由於同一程式的多個執行緒共享同一地址空間,因此Text Segment、Data Segment都是共享的,如果定義一個函式,在各執行緒中都可以呼叫,如果定義一個全域性變數,在各執行緒中都可以訪問到,除此之外,各執行緒還共享以下程式資源和環境:

檔案描述符表

每種訊號的處理方式(SIG_IGN、SIG_DFL或者自定義的訊號處理函式)

當前工作目錄

使用者id和組id

但有些資源是每個執行緒各有一份的:

執行緒id

上下文,包括各種暫存器的值、程式計數器和棧指標

棧空間

errno變數

訊號遮蔽字

排程優先順序

 

我們將要學習的執行緒庫函式是由POSIX標準定義的,稱為POSIX thread或者pthread。在Linux上執行緒函式位於libpthread共享庫中,因此在編譯時要加上-lpthread選項。

 

1. 建立執行緒

#include <pthread.h>
 
int pthread_create(pthread_t *restrictthread,
         constpthread_attr_t *restrict attr,
         void*(*start_routine)(void*), void *restrict arg);

返回值:成功返回0,失敗返回錯誤號。以前學過的系統函式都是成功返回0,失敗返回-1,而錯誤號儲存在全域性變數errno中,而pthread庫的函式都是通過返回值返回錯誤號,雖然每個執行緒也都有一個errno,但這是為了相容其它函式介面而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。

 

在一個執行緒中呼叫pthread_create()建立新的執行緒後,當前執行緒從pthread_create()返回繼續往下執行,而新的執行緒所執行的程式碼由我們傳給pthread_create的函式指標start_routine決定。start_routine函式接收一個引數,是通過pthread_create的arg引數傳遞給它的,該引數的型別為void *,這個指標按什麼型別解釋由呼叫者自己定義。start_routine的返回值型別也是void *,這個指標的含義同樣由呼叫者自己定義。start_routine返回時,這個執行緒就退出了,其它執行緒可以呼叫pthread_join得到start_routine的返回值,類似於父程式呼叫wait(2)得到子程式的退出狀態,稍後詳細介紹pthread_join。

 

pthread_create成功返回後,新建立的執行緒的id被填寫到thread引數所指向的記憶體單元。我們知道程式id的型別是pid_t,每個程式的id在整個系統中是唯一的,呼叫getpid(2)可以獲得當前程式的id,是一個正整數值。執行緒id的型別是thread_t,它只在當前程式中保證是唯一的,在不同的系統中thread_t這個型別有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf列印,呼叫pthread_self(3)可以獲得當前執行緒的id。

 

attr參數列示執行緒屬性,本章不深入討論執行緒屬性,所有程式碼例子都傳NULL給attr引數,表示執行緒屬性取預設值。首先看一個簡單的例子:

 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
 
pthread_t ntid;
 
void printids(const char *s)
{
         pid_t      pid;
         pthread_t  tid;
 
         pid= getpid();
         tid= pthread_self();
         printf("%spid %u tid %u (0x%x)\n", s, (unsigned int)pid,
                (unsigned int)tid, (unsigned int)tid);
}
 
void *thr_fn(void *arg)
{
         printids(arg);
         returnNULL;
}
 
int main(void)
{
         interr;
 
         err= pthread_create(&ntid, NULL, thr_fn, "new thread: ");
         if(err != 0) {
                   fprintf(stderr,"can't create thread: %s\n", strerror(err));
                   exit(1);
         }
         printids("mainthread:");
         sleep(1);
 
         return0;
}

編譯執行結果如下:

 

main thread: pid 7398 tid 3084450496(0xb7d8fac0)
new thread: pid 7398 tid 3084446608 (0xb7d8eb90)

可知在Linux上,thread_t型別是一個地址值,屬於同一程式的多個執行緒呼叫getpid(2)可以得到相同的程式號,而呼叫pthread_self(3)得到的執行緒號各不相同。

 

由於pthread_create的錯誤碼不儲存在errno中,因此不能直接用perror(3)列印錯誤資訊,可以先用strerror(3)把錯誤碼轉換成錯誤資訊再列印。

 

 2. 終止執行緒

如果需要只終止某個執行緒而不終止整個程式,可以有三種方法:

 從執行緒函式return。這種方法對主執行緒不適用,從main函式return相當於呼叫exit。

 一個執行緒可以呼叫pthread_cancel終止同一程式中的另一個執行緒。

 執行緒可以呼叫pthread_exit終止自己。

 

用pthread_cancel終止一個執行緒分同步和非同步兩種情況,比較複雜,本章不打算詳細介紹,讀者可以參考[APUE2e]。下面介紹pthread_exit的和pthread_join的用法。

 

#include <pthread.h>
void pthread_exit(void *value_ptr);

value_ptr是void *型別,和執行緒函式返回值的用法一樣,其它執行緒可以呼叫pthread_join獲得這個指標。

需要注意,pthread_exit或者return返回的指標所指向的記憶體單元必須是全域性的或者是用malloc分配的,不能線上程函式的棧上分配,因為當其它執行緒得到這個返回指標時執行緒函式已經退出了。

 

#include <pthread.h>
int pthread_join(pthread_t thread, void**value_ptr);

返回值:成功返回0,失敗返回錯誤號

呼叫該函式的執行緒將掛起等待,直到id為thread的執行緒終止。thread執行緒以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:

如果thread執行緒通過return返回,value_ptr所指向的單元裡存放的是thread執行緒函式的返回值。

如果thread執行緒被別的執行緒呼叫pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED。

如果thread執行緒是自己呼叫pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的引數。

如果對thread執行緒的終止狀態不感興趣,可以傳NULL給value_ptr引數。


看下面的例子 :

 

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
 
void *thr_fn1(void *arg)
{
         printf("thread1 returning\n");
         return(void *)1;
}
 
void *thr_fn2(void *arg)
{
         printf("thread2 exiting\n");
         pthread_exit((void*)2);
}
 
void *thr_fn3(void *arg)
{
         while(1){
                   printf("thread3 writing\n");
                   sleep(1);
         }
}
 
int main(void)
{
         pthread_t   tid;
         void        *tret;
 
         pthread_create(&tid,NULL, thr_fn1, NULL);
         pthread_join(tid,&tret);
         printf("thread1 exit code %d\n", (int)tret);
 
         pthread_create(&tid,NULL, thr_fn2, NULL);
         pthread_join(tid,&tret);
         printf("thread2 exit code %d\n", (int)tret);
 
         pthread_create(&tid,NULL, thr_fn3, NULL);
         sleep(3);
         pthread_cancel(tid);
         pthread_join(tid,&tret);
         printf("thread3 exit code %d\n", (int)tret);
 
         return0;
}

執行結果是:

 

thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1

 可見在Linux的pthread庫中常數PTHREAD_CANCELED的值是-1。可以在標頭檔案pthread.h中找到它的定義:

#define PTHREAD_CANCELED ((void *) -1)一般情況下,執行緒終止後,其終止狀態一直保留到其它執行緒呼叫pthread_join獲取它的狀態為止。但是執行緒也可以被置為detach狀態,這樣的執行緒一旦終止就立刻回收它佔用的所有資源,而不保留終止狀態。不能對一個已經處於detach狀態的執行緒呼叫pthread_join,這樣的呼叫將返回EINVAL。對一個尚未detach的執行緒呼叫pthread_join或pthread_detach都可以把該執行緒置為detach狀態,也就是說,不能對同一執行緒呼叫兩次pthread_join,或者如果已經對一個執行緒呼叫了pthread_detach就不能再呼叫pthread_join了。

 

#include <pthread.h>
int pthread_detach(pthread_t tid);

返回值:成功返回0,失敗返回錯誤號。

 

 

 

 

 

 

 

 

 

 

 

相關文章