UNP-UNIX網絡編程 第二十六章:多線程

fork是昂貴的(把父進程的內存映像複製到子進程),並且需要進程間通信(IPC)機制。
線程的創建速度快(10-100 倍),同一進程中的線程共享相同的全局內存,線程之間容易共享信息,但是,這就帶來了同步的問題。

同一進程內的所有線程除了共享全局變量,還共享:
進程指令,大多數數據,打開的文件(即文件描述符),信號處理函數和信號處置,當前工作目錄,用戶ID和組ID,”這些很容易出錯誤”

不過每個線程有各自的:線程ID,優先級,棧(用於存放局部變量和返回地址),寄存器集合(包括程序計數器和棧指針),errno,信號掩碼。

(一)POSIX線程pthread:

1. 線程初始化:

//pthread_create()類似fork()
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void *(*func)(void*), void *arg);//成功返回0,出錯則爲整的Exxx值

注:每個線程都有許多屬性(attribute):線程ID,優先級,初始棧大小,是否應該成爲一個守護線程,等,通常情況下我們採納默認設置,把attr參數指定爲NULL
func是該線程執行的函數,arg是該函數的參數,如果是多個參數,就需要打包成一個結構。

2. 等待線程結束:pthread_join()類似waitpid(可以不指定id,令ID=-1,來等待任意id的進程)

int pthread_join(pthread_t *tid, void **status);//成功返回0,出錯則爲整的Exxx值,必須指定tid

通過調用pthread_join等待一個給定的線程終止。如果status指針非空,來自所等待線程的返回值(一個指向某個對象的指針)將存入由status指向的位置。

3. 獲取自身ID:pthread_self類似getpid

pthread_t pthread_self();//返回調用線程的線程ID

4 .脫離線程:pthread_detach

int pthread_detach(pthread tid);//成功返回0,出錯則爲整的Exxx值

注:與pthread_join相反,當一個joinable(可匯合)線程終止時,它的線程ID和退出狀態將留存到另一個線程對它調用pthread_join。
而detached線程卻像守護進程,當它們終止時,所有相關資源都被釋放,我們不能等待它們終止。
如果一個線程需要知道另一個線程什麼時候終止,那就最好保持第二個線程的可匯合(joinable)狀態。

pthread_detach(pthread_self());//讓自己脫離

5.pthread_exit函數:讓一個線程終止

void pthread_exit(void *status)

對於可匯合的線程,它的線程ID和退出狀態將留存到另一個線程對它調用pthread_join(),
另外兩個終止線程的方法;
(1)啓動線程的函數(creat的第三個參數)返回
(2)進程的main函數返回,或者線程調用了exit

6. 多線程客戶端

#include    "unpthread.h"//unp.h和包裹函數們Pthread
void    *copyto(void *);
static int  sockfd;     /* global for both threads to access */
static FILE *fp;
void str_cli(FILE *fp_arg, int sockfd_arg)
{
    char        recvline[MAXLINE];
    pthread_t   tid;

    sockfd = sockfd_arg;//線程外部變量,套接字描述符
    fp = fp_arg;//和標準I/O FILE指針

    Pthread_create(&tid, NULL, copyto, NULL);//創建線程,新線程id返回到tid,
//copyto從標準輸入讀到EOF先於main函數終止。子線程在執行copyto同時,主線程就去執行下面了。
    while (Readline(sockfd, recvline, MAXLINE) > 0)//主線程循環
        Fputs(recvline, stdout);//文本行從套接字到標準輸出
}//終止進程,進程內所有線程也被終止

void* copyto(void* arg)//子線程
{
    char    sendline[MAXLINE];

    while (Fgets(sendline, MAXLINE, fp) != NULL)//標準輸入拷貝到套接字,
        Writen(sockfd, sendline, strlen(sendline));

    Shutdown(sockfd, SHUT_WR);//當在標準輸入讀到EOF時,調用shutdown從套接字送出FIN

    return(NULL);//終止子線程
        /* 4return (i.e., thread terminates) when EOF on stdin */
}

7. 多線程服務器

#include    "unpthread.h"

static void *doit(void *);      /* each thread executes this function */

int main(int argc, char **argv)//每個客戶使用一個線程
{
    int             listenfd, //*iptr;
    pthread_t       tid;
    socklen_t       addrlen, len;
    struct sockaddr *cliaddr;

    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");

    cliaddr = Malloc(addrlen);

    for ( ; ; ) {//使用Pthread_create代替fork
        len = addrlen;
        iptr = Malloc(sizeof(int));//每次循環都開闢新的空間,不會用到全局的值
        int *iptr = Accept(listenfd, cliaddr, &len);
        Pthread_create(&tid, NULL, &doit, iptr);//函數doit和參數-已連接套接字描述符connfd,最後一個參數傳值(指針拷貝)
    }
}

static void* doit(void* arg)//子線程arg就是參數iptr
{
    int connfd = *((int*)arg);//void* -> int*,然後再解引用
    free arg;//arg已經完成任務了
    Pthread_detach(pthread_self());//自身脫離,主線程沒有理由等待他所創建的子線程
    str_echo(connfd);   /* same function as before */
    Close(connfd);//線程關閉,已連接套接字不一定被Close;在進程中,子進程結束,套接字就close了
    return(NULL);
}

8. 給新線程傳遞參數

把connfd強制轉換爲void並不保證一定起作用,也不能直接把connfd的地址傳遞給新線程

9. 線程安全:圖 26-5 線程安全版本

gethostbyname 和 gethostbyaddr不可重入,但是有可重入的_r版本,要注意的是,這不是標準,所有不可衝入的函數要慎用(表 11-21)

10. 線程特定數據

當把非線程的傳旭寫成線程的版本時,會碰到函數中使用靜態變量的情況,從而引起常見的編譯錯誤。在不考慮衝入的情況下,靜態變量無可非議,但是在同一個進程中的不同線程幾乎同時調用這樣的函數,就會出現錯誤,後果是不確定的,因爲這些靜態變量無法爲不同的線程保存各自的值。
解決方法:
(1)使用線程特定數據,這樣轉換成了只能在多線程系統上工作的函數。
每個系統支持有限的線程特定數據元素。POSIX要求這個限制不小於128(每個進程)。系統爲每個進程維護一個我們稱之爲key結構的結構數組
key結構中的標誌指示這個數據元素是否正在使用,所有的標誌初始化爲“不在使用”。當一個線程調用pthread_key_create創建一個新的線程特定數據元素時,系統會返回第一個不在使用的元素。key結構中的析構函數指針,當一個線程終止時,系統將掃描該線程的pkey數組,爲每個非空的pkey指針調用相應的析構函數。
除了進程範圍的key結構數組外,系統還在進程內維護關於每個線程的多條信息。這些特定於線程的信息我們稱之爲pthread結構,其部分內容是我們稱之爲pkey數組的一個128個元素的指針數組。
注意當我們調用pthread_key_create創建一個鍵時,系統告訴我們這個鍵。每個線程可以隨後爲該鍵存儲一個值(指針),而這個指針通常是每個線程通過malloc獲得的。具體的函數如下:

#include <pthread.h>  
int pthread_once(pthread_once_t *onceptr, void (*init)(void));  
int pthread_key_create(pthread_key_t *keyptr, void (*destructor)(void *value));  
void *pthread_getspecific(pthread_key_t key);  
int pthread_setspecific(pthread_key_t key, const void *value);
不管多少個線程只有第一個執行它的線程運行一次被掉函數,保證了分配的rl_key的安全。 
if ((ptr = pthread_getspecific(rl_key)) == NULL) // 檢查當前線程key域是否有值
pthread_setspecific(rl_key, ptr);// 設置當前線程的pthread結構key的值  
void destructor(void *value) 
{  
    printf("pthread:%d destructor value(%p)\n", pthread_self(), value);  
    free(value);// 線程結束後執行。線程結束後,會自動掉此析構函數,釋放分配資源。 
}  

(2)改變調用順序,把函數參數和靜態變量都放在一個結構中,可以再支持/不支持線程的系統上使用,但是調用函數的所有應用程序都要更改。
(3)改變接口結構,避免使用靜態變量

(二)互斥鎖:開銷並不大

父子進程除了描述符外不共享其餘的任何東西。
多個線程更改一個共享變量,解決方法是使用一個互斥鎖,保護這個共享變量,訪問該變量的條件是持有互斥鎖

int pthread_mutex_lock(pthread_mutex_t *mptr);//上鎖
int pthread_mutex_unlock(pthread_mutex_t *mptr);//解鎖
//成功返回0,失敗返回正值Exxx值

如果試圖上鎖一個已經被另外一個線程鎖住的一個互斥鎖,本線程被阻塞,知道該互斥鎖被解鎖。
如果某個互斥鎖變量是靜態分配的,我們就必須把它初始化爲常量PTHREAD_MUTEX_INITIALIZER。如果我們在共享內存區分配一個互斥鎖,那麼必須通過調用pthread_mutex_init函數在運行時把它初始化。
在每個子線程內:

pthread_mutex_t lock;  
pthread_mutex_lock(&lock);  
i++;  
Pthread_mutex_unlock(&lock); 

(三)條件變量
互斥鎖用於防止多個線程同時訪問某個變量,但我們還需要在等待某個條件(事件)發生期間能讓我們進入睡眠的機制。
如果沒有這個機制,線程在等待一個條件發生期間只能輪詢,這顯然非常浪費CPU資源。條件變量加上互斥鎖就能實現這種機制。條件變量的類型是pthread_cond_t。條件變量API如下:

/*等待一個條件變量,線程進入睡眠狀態*/  
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);  
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr,  
   const struct timespec *abstime);//時刻而不是時間  

/*喚醒等待在條件變量上的一個線程*/  
int pthread_cond_signal(pthread_cond_t *cptr);  
/*喚醒等待在條件變量上的所有線程*/  
int pthread_cond_broadcast(pthread_cond_t *cptr);  

舉個例子,我們使用一個全局變量flag標誌一個事件是否發生。線程A測試flag如果爲0,表明事件未發生則睡眠等待。線程B產生這個事件然後將flag標誌置1,喚醒線程A。爲此我們定義了以下三個變量:

int flag;  //計數器
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  //互斥鎖
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;  //條件變量
線程A使用如下的代碼等待事件發生:
pthread_mutex_lock(&mutex);  
while (flag == 0)  
    pthread_cond_wait(&cond, &mutex); /*睡眠等待事件發生*///解鎖,cond調用signal之後加鎖
    /*下一步的動作*/  
pthread_mutex_unlock(&mutex);  
線程B使用如下的代碼產生事件並喚醒線程A:
/*某個事件發生*/  
pthread_mutex_lock(&mutex);  
flag = 1;  
pthread_cond_signal(&cond); /*喚醒線程A*/  
pthread_mutex_unlock(&mutex);  

條件變量總是和一個表示“條件”的全局變量關聯,在此例中即是flag變量,flag值爲0表明條件不成立(事件還未發生)。
全局變量總是需要互斥鎖保護,因此互斥鎖和條件變量經常一起使用。這也解釋了爲什麼pthread_cond_wait函數的兩個參數一個是條件變量一個是互斥鎖。
另外,由於測試條件之前總是先加鎖,所以當條件不成立時pthread_cond_wait函數必須先解鎖,然後把調用線程投入睡眠。
當線程被喚醒時,它又再次加鎖,然後返回。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章