UNIX多執行緒程式設計

weixin_34198583發表於2016-01-25

一個程式至少有一個程式。一個程式至少有一個執行緒。程式擁有自己獨立的儲存空間,而執行緒能夠看作是輕量級的程式,共享程式內的全部資源。能夠把程式看作一個工廠。執行緒看作工廠內的各個車間,每一個車間共享整個工廠內的全部資源。就像每一個程式有一個程式ID一樣,每一個執行緒也有一個執行緒ID,程式ID在整個系統中是唯一的。但執行緒ID不同,執行緒ID僅僅在它所屬的程式環境中有效。

執行緒ID的資料型別為pthread_t,一般是無符號長整型。

typedef unsigned long int pthread_t; // pthreadtypes.h

GCC編譯執行緒相關檔案時。要使用-pthread引數。先介紹兩個實用的函式:

#include <pthread.h> 

pthread_t pthread_self(void); 
int pthread_equal(pthread_t t1, pthread_t t2);

pthread_self函式獲取自身的執行緒ID。pthread_equal函式比較兩個執行緒ID是否相等。這樣做的目的是提高程式碼的可移植性。由於不同系統的pthread_t型別可能不同。

1、怎樣建立執行緒

建立執行緒呼叫pthread_create函式:

#include <pthread.h> 

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                    void *(*start_routine) (void *), void *arg);

若成功則返回0,否則返回錯誤編號。當pthread_create成功返回時,由thread引數指向的記憶體單元被設定為新建立執行緒的執行緒ID。

attr引數用於定製各種不同的執行緒屬性,可設定為NULL,建立預設屬性的執行緒。新建立的執行緒從start_routine函式的地址開始執行,該函式僅僅有一個無型別指標引數arg,假設須要向start_routine函式傳遞的引數不止一個,那麼須要把這些引數放到一個結構中,然後把這個結構的地址作為arg引數傳入。

執行緒新建立時並不能保證哪個執行緒會先執行,是新建立的執行緒還是呼叫執行緒。新建立的執行緒能夠訪問程式的地址空間,並且繼承呼叫執行緒的浮點環境和訊號遮蔽字,可是該執行緒的未決訊號集被清除。pthread函式在呼叫失敗時一般會返回錯誤碼,它們並不像其他的POSIX函式一樣設定errno。

每一個執行緒都提供errno副本,這僅僅是為了與使用errno的現有函式相容。線上程中,從函式中返回錯誤碼更為清晰整潔,不須要依賴那些隨著函式執行不斷變化的全域性狀態,由於能夠把錯誤的範圍限制在引起出錯的函式中。

以下看一個樣例pthreadEx.c:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 

pthread_t g_tid; 

void printIds(const char *s) 
{ 
    pid_t pid; 
    pthread_t tid; 
    pid = getpid(); 
    tid = pthread_self(); 
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, 
        (unsigned int)tid, (unsigned int)tid); 
} 

void* threadFunc(void *arg) 
{ 
    printIds("new thread: "); 
    return ((void*)0); 
} 

int main(void) 
{ 
    int err; 
    err = pthread_create(&g_tid, NULL, threadFunc, NULL); 
    if (0 != err) { 
        printf("can't create thread: %s\n", strerror(err)); 
        abort();         
    } 
    printIds("main thread: "); 
    sleep(1); 
    exit(0); 
} 

GCC編譯:

gcc -o thread pthreadEx.c -pthread

執行結果:

./thread
main thread:  pid 5703 tid 1397368640 (0x534a2740) 
new thread:  pid 5703 tid 1389127424 (0x52cc6700) 

上面樣例中,使用sleep處理主執行緒和新執行緒之間的競爭。防止新執行緒執行之前主執行緒已經退出,這樣的行為特徵依賴於作業系統中的執行緒實現和排程演算法。新執行緒ID的獲取是通過pthread_self函式而不是g_tid全域性變數,原因是主執行緒把新執行緒ID存放在g_tid中。可是新執行緒可能在主執行緒呼叫pthread_create返回之前就開始執行了,這時g_tid的內容是不對的。從上面的樣例能夠看出,兩個執行緒的pid是同樣的,不同的是tid,這是合理的,但兩個執行緒的輸出順序是不定的,另外執行結果在不同的作業系統下可能是不同的。這依賴於詳細的實現。

2、怎樣終止執行緒

假設程式中的任一執行緒呼叫了exit、_Exit或者_exit。這時整個程式就會終止。而不單單是呼叫執行緒。與此相似。假設訊號的預設動作是終止程式。把這個訊號傳送到某個執行緒也會終止整個程式。那麼。單個執行緒怎樣退出呢?

(1)執行緒僅僅是從啟動例程中返回,返回值是執行緒的退出碼。


(2)執行緒能夠被同一程式中的其他執行緒取消。


(3)執行緒呼叫pthread_exit函式。

retval是一個無型別的指標,與傳給啟動例程的單個引數相似,程式中的其他執行緒能夠通過呼叫pthread_join函式訪問到這個指標。呼叫執行緒將一直堵塞,直到指定的執行緒呼叫pthread_exit、從啟動例程中返回或者被取消。假設執行緒僅僅是從它的啟動例程返回,retval將包括返回碼。假設執行緒被取消。由retval指定的記憶體單元就置為PTHREAD_CANCELED。能夠通過呼叫pthread_join自己主動把執行緒置於分離狀態,這樣資源就能夠恢復。假設執行緒已經處於分離狀態,pthread_join呼叫就會失敗,返回EINVAL。

假設對執行緒的返回值並不感興趣,能夠把retval置為NULL。在這樣的情況下。呼叫pthread_join函式將等待指定的執行緒終止。但並不獲取執行緒的終止狀態。

#include <pthread.h> 

void pthread_exit(void *retval);
int pthread_join(pthread_t thread, void **retval);

且看以下的樣例pthreadRet.c:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 

void* threadFunc1(void *arg) 
{ 
    printf("thread 1 returning\n"); 
    return (void*)1; 
} 

void* threadFunc2(void *arg) 
{ 
    printf("thread 2 exiting\n"); 
    pthread_exit((void*)2); 
} 

int main(void) 
{ 
    int err; 
    pthread_t tid1, tid2; 
    void *ret; 

    err = pthread_create(&tid1, NULL, threadFunc1, NULL); 
    if (0 != err) { 
        printf("can't create thread 1: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid2, NULL, threadFunc2, NULL); 
    if (0 != err) { 
        printf("can't create thread 2: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_join(tid1, &ret); 
    if (0 != err) { 
        printf("can't join with thread 1: %s\n", strerror(err)); 
        abort();         
    } 
    printf("thread 1 exit code %x\n", (int)ret); 

    err = pthread_join(tid2, &ret); 
    if (0 != err) { 
        printf("can't join with thread 2: %s\n", strerror(err)); 
        abort();         
    } 
    printf("thread 2 exit code %d\n", (int)ret); 

    exit(0); 
} 

執行結果:

thread 2 exiting 
thread 1 returning 
thread 1 exit code 1 
thread 2 exit code 2 

從上面的樣例中能夠看出。當一個執行緒呼叫pthread_exit退出或者簡單地從啟動例程中返回時,程式中的其他執行緒能夠通過呼叫pthread_join函式獲得該執行緒的退出狀態。

pthread_create和pthread_exit函式的無型別指標引數能傳遞的數值能夠不止一個,該指標能夠傳遞包括更復雜資訊的結構的地址,可是注意這個結構所使用的記憶體在呼叫者完畢呼叫以後必須仍然是有效的,否則就會出現無效或非法記憶體訪問。以下的樣例pthreadProb.c使用了分配在棧上的自己主動變數。說明了這個問題:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 

struct foo 
{ 
    int a, b, c, d; 
}; 

void printFoo(const char *s, const struct foo *fp) 
{ 
    printf(s); 
    printf(" structure at 0x%x\n", (unsigned)fp); 
    printf(" foo.a = %d\n", fp->a); 
    printf(" foo.b = %d\n", fp->b); 
    printf(" foo.c = %d\n", fp->c); 
    printf(" foo.d = %d\n", fp->d); 
} 

void* threadFunc1(void *arg) 
{ 
    struct foo foo = {1, 2, 3, 4}; 
    printFoo("thread 1:\n", &foo); 
    pthread_exit((void*)&foo); 
} 

void* threadFunc2(void *arg) 
{ 
    printf("thread 2: ID is %u\n", (unsigned int)pthread_self()); 
    pthread_exit((void*)0); 
} 

int main(void) 
{ 
    int err; 
    pthread_t tid1, tid2; 
    struct foo *fp; 

    err = pthread_create(&tid1, NULL, threadFunc1, NULL); 
    if (0 != err) { 
        printf("can't create thread 1: %s\n", strerror(err)); 
        abort();         
    } 
    err = pthread_join(tid1, (void*)&fp); 
    if (0 != err) { 
        printf("can't join with thread 1: %s\n", strerror(err)); 
        abort();         
    } 
    sleep(1); 
    printf("parent starting second thread\n"); 

    err = pthread_create(&tid2, NULL, threadFunc2, NULL); 
    if (0 != err) { 
        printf("can't create thread 2: %s\n", strerror(err)); 
        abort();         
    } 
    sleep(1); 
    printFoo("parent: \n", fp); 

    exit(0); 
} 

輸出結果:

thread 1: 
 structure at 0xb18eae90 
 foo.a = 1 
 foo.b = 2 
 foo.c = 3 
 foo.d = 4 
parent starting second thread 
thread 2: ID is 119400192 
parent: 
 structure at 0xb18eae90 
 foo.a = 0 
 foo.b = 0 
 foo.c = 1 
 foo.d = 0

所以,為了避免這個問題。能夠使用全域性結構,或者呼叫malloc函式分配結構。
執行緒能夠通過呼叫pthread_cancel函式來請求取消同一程式中的其他執行緒。

#include <pthread.h> 

int pthread_cancel(pthread_t thread); 

在預設的情況下。pthread_cancel函式會使得thread引數標識的執行緒的行為表現為如同呼叫了引數為PTHREAD_CANCELED的pthread_exit函式,可是執行緒能夠選擇忽略取消方式或是控制取消方式。pthread_cancel並不等待執行緒終止,它僅僅提出請求。

執行緒能夠安排它退出時須要呼叫的函式。這與程式能夠用atexit函式安排程式退出是須要呼叫的函式是相似的,這樣的函式稱為執行緒清理處理程式。執行緒能夠建立多個清理處理程式,處理程式記錄在棧中。也就是說它們的執行順序與它們註冊的順序相反。

#include <pthread.h> 

void pthread_cleanup_push(void (*routine)(void *), void *arg); 
void pthread_cleanup_pop(int execute);

當執行緒執行以下動作時呼叫清理函式,呼叫引數為arg。清理函式routine的呼叫順序是由pthread_cleanup_push函式來安排的。

(1)呼叫phtread_exit時。
(2)響應取消請求時。


(3)用非零execute引數呼叫pthread_cleanup_pop時。假設execute引數為0,清理函式將不被呼叫。

不管哪種情況。pthread_cleanup_pop都將刪除上次pthread_cleanup_push呼叫建立的清理處理程式。

以下的樣例pthreadClean.c。僅僅呼叫了第二個執行緒的清理處理程式,原因是第一個執行緒是通過從它的啟動例程中返回而終止,而非上面提到的三個呼叫清理處理程式的條件之中的一個。

// pthreadClean.c 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 

void cleanup(void *arg) 
{ 
    printf("cleanup: %s\n", (char*)arg); 
} 

void* threadFunc1(void *arg) 
{ 
    printf("thread 1 start\n"); 
    pthread_cleanup_push(cleanup, "thread 1 first handler"); 
    pthread_cleanup_push(cleanup, "thread 1 second handler"); 
    printf("thread 1 push complete\n"); 
    if (arg) { 
        return (void*)1; 
    } 
    pthread_cleanup_pop(0); 
    pthread_cleanup_pop(0); 
    return (void*)1; 
} 

void* threadFunc2(void *arg) 
{ 
    printf("thread 2 start\n"); 
    pthread_cleanup_push(cleanup, "thread 2 first handler"); 
    pthread_cleanup_push(cleanup, "thread 2 second handler"); 
    printf("thread 2 push complete\n"); 
    if (arg) { 
        pthread_exit((void*)2); 
    } 
    pthread_cleanup_pop(0); 
    pthread_cleanup_pop(0); 
    pthread_exit((void*)2); 
} 

int main(void) 
{ 
    int err; 
    pthread_t tid1, tid2; 
    void *ret; 

    err = pthread_create(&tid1, NULL, threadFunc1, (void*)1); 
    if (0 != err) { 
        printf("can't create thread 1: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid2, NULL, threadFunc2, (void*)1); 
    if (0 != err) { 
        printf("can't create thread 2: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_join(tid1, &ret); 
    if (0 != err) { 
        printf("can't join with thread 1: %s\n", strerror(err)); 
        abort();         
    }    
    printf("thread 1 exit code %d\n", (int)ret); 


    err = pthread_join(tid2, &ret); 
    if (0 != err) { 
        printf("can't join with thread 2: %s\n", strerror(err)); 
        abort();         
    } 
    printf("thread 2 exit code %d\n", (int)ret); 

    exit(0); 
} 

執行結果:

thread 1 start 
thread 1 push complete 
thread 2 start 
thread 2 push complete 
thread 1 exit code 1 
cleanup: thread 2 second handler 
cleanup: thread 2 first handler 
thread 2 exit code 2

在預設情況下,執行緒的終止狀態會儲存到對該執行緒呼叫pthread_join,假設執行緒已經處於分離狀態,執行緒的底層儲存資源能夠線上程終止時馬上被收回。當執行緒被分離時,並不能用pthread_join函式等待它的終止狀態。對分離狀態的執行緒進行pthread_join的呼叫會產生失敗,返回EINVAL。

pthread_detach呼叫能夠用於使執行緒進入分離狀態。

#include <pthread.h> 

int pthread_detach(pthread_t thread);

3、執行緒同步

執行緒同步是一個非常重要的概念。當多個執行緒同一時候改動或訪問一塊記憶體時。假設沒有保護措施非常easy發生衝突,這時就須要使用執行緒同步技術,以下介紹三種執行緒同步技術:相互排斥量讀寫鎖條件變數

(1)相互排斥量mutex

#include <pthread.h>

int pthread_mutex_init (pthread_mutex_t *mutex,
                const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy (pthread_mutex_t *mutex);
int pthread_mutex_trylock (pthread_mutex_t *mutex)
int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_unlock (pthread_mutex_t *mutex);

能夠通過使用pthread的相互排斥介面保護資料,確保同一時間僅僅有一個執行緒訪問資料。相互排斥量mutex從本質上說是一把鎖。在訪問共享資源前對相互排斥量進行加鎖。在訪問完畢後釋放相互排斥量上的鎖。對相互排斥量進行加鎖以後,不論什麼其他試圖再次對相互排斥量加鎖的執行緒將會被堵塞直到當前執行緒釋放該相互排斥鎖。

假設釋放相互排斥鎖時有多個執行緒堵塞,全部在該相互排斥鎖上的堵塞執行緒都會變成可執行狀態,第一個變為執行狀態的執行緒能夠對相互排斥量加鎖。其他執行緒將會看到相互排斥鎖依舊被鎖住,僅僅能回去再次等待它又一次變為可用。在這樣的方式下。每次僅僅有一個執行緒能夠向前執行。

在設計時須要規定全部的執行緒必須遵守同樣的資料訪問規則。僅僅有這樣,相互排斥機制才幹正常工作。

作業系統並不會做資料訪問的序列化。

假設同意當中的某個執行緒在沒有得到鎖的情況下也能夠訪問共享資源。那麼即使其他的執行緒在使用共享資源前都獲取了鎖,也還是會出現資料不一致的問題。

相互排斥變數用pthread_mutex_t資料型別來表示,在使用相互排斥變數曾經,必須首先對它進行初始化。能夠把它置為常量PTHREAD_MUTEX_INITIALIZER。這個是僅僅對靜態分配的相互排斥量。也能夠通過呼叫pthread_mutex_init函式進行初始化。要用預設的屬性初始化相互排斥量,僅僅需把mutexattr設定為NULL。假設動態地分配相互排斥量,比如通過呼叫malloc函式。那麼在釋放記憶體前須要呼叫pthread_mutex_destroy函式。

對相互排斥量進行加鎖,須要呼叫pthread_mutex_lock。假設相互排斥量已經上鎖,呼叫執行緒將堵塞直到相互排斥量被鎖住。

對相互排斥量解鎖。須要呼叫pthread_mutex_unlock。假設執行緒不希望被堵塞,它能夠使用pthread_mutex_trylock嘗試對相互排斥量進行加鎖。假設呼叫pthread_mutex_trylock時相互排斥量處於未鎖住狀態,那麼pthread_mutex_trylock將鎖住相互排斥量,不會出現堵塞並返回0,否則pthread_mutex_trylock就會失敗,不能鎖住相互排斥量,而返回EBUSY。

以下是一個使用相互排斥量保護資料結構的樣例:

#include <stdlib.h> 
#include <pthread.h> 

struct foo { 
    int f_count; 
    pthread_mutex_t f_lock; 
}; 

struct foo* foo_alloc(void) 
{ 
    struct foo *fp; 

    if ((fp = malloc(sizeof(struct foo))) != NULL) { 
        fp->f_count = 1; 
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { 
            free(fp); 
            return NULL; 
        } 
    } 
    return fp; 
} 

void foo_hold(struct foo *fp) 
{ 
    pthread_mutex_lock(&fp->f_lock); 
    fp->f_count++; 
    pthread_mutex_unlock(&fp->f_lock); 
} 

void foo_release(struct foo *fp) 
{ 
    pthread_mutex_lock(&fp->f_lock); 
    if (--(fp->f_count) == 0) { 
        pthread_mutex_unlock(&fp->f_lock); 
        pthread_mutex_destroy(&fp->f_lock); 
        free(fp); 
    } 
    else { 
        pthread_mutex_unlock(&fp->f_lock); 
    } 
}

在對引用計數加1、減1以及檢查引用計數是否為0這些操作之前須要鎖住相互排斥量。在foo_alloc函式將引用計數初始化為1時不是必需加鎖,由於在這個操作之前分配執行緒是唯一引用該物件的執行緒。可是在這之後假設要將該物件放到一個列表中,那麼它就有可能被別的執行緒發現。因此須要首先對它加鎖。在使用該物件前,執行緒須要對這個物件的引用計數加1。當物件使用完畢時,須要對引用計數減1。當最後一個引用被釋放時。物件所佔的記憶體空間就被釋放。

死鎖——
使用相互排斥量時。一個非常重要發問題就是避免死鎖。

比如,假設執行緒試圖對同一個相互排斥量加鎖兩次,那麼它自身就會陷入死鎖狀態。假設兩個執行緒都在相互請求還有一個執行緒擁有的資源。那麼這兩個執行緒都無法向前執行,也會產生死鎖。

一個經常使用的避免死鎖的方法是控制相互排斥量加鎖的順序,全部執行緒總是對幾個相互排斥量的加鎖順序保持一致;有時候加鎖順序難以控制,我們會先釋放自己已經佔有的鎖,然後去嘗試獲取別的鎖。

以下的樣例使用了兩個相互排斥量,加鎖順序同樣,hashlock相互排斥量保護foo資料結構中的fh雜湊表和f_next雜湊鏈欄位,foo結構中的f_lock相互排斥量保護對foo結構中的其他欄位的訪問。

#include <stdlib.h> 
#include <pthread.h> 

#define NHASH 29 
#define HASH(fp) (((unsigned long)fp)%NHASH) 
struct foo* fh[NHASH]; 

pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER; 

struct foo { 
    int f_count; 
    pthread_mutex_t f_lock; 
    struct foo *f_next; 
    int f_id; 
}; 

struct foo* foo_alloc(void) 
{ 
    struct foo *fp; 
    int idx; 

    if ((fp = malloc(sizeof(struct foo))) != NULL) { 
        fp->f_count = 1; 
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { 
            free(fp); 
            return NULL; 
        } 
        idx = HASH(fp); 
        pthread_mutex_lock(&hashlock); 
        fp->f_next = fh[idx]; 
        fh[idx] = fp->f_next; 
        pthread_mutex_lock(&fp->f_lock); 
        pthread_mutex_unlock(&hashlock); 
        pthread_mutex_unlock(&fp->f_lock); 
    } 
    return fp; 
} 

void foo_hold(struct foo *fp) 
{ 
    pthread_mutex_lock(&fp->f_lock); 
    fp->f_count++; 
    pthread_mutex_unlock(&fp->f_lock); 
} 

struct foo* foo_find(int id) 
{ 
    struct foo *fp; 
    int idx; 
    idx = HASH(fp); 
    pthread_mutex_lock(&hashlock); 
    for (fp = fh[idx]; fp != NULL; fp = fp->f_next) { 
        if (fp->f_id == id) { 
            foo_hold(fp); 
            break; 
        } 
    } 
    pthread_mutex_unlock(&hashlock); 
    return fp; 
} 


void foo_release(struct foo *fp) 
{ 
    struct foo *tfp; 
    int idx; 

    pthread_mutex_lock(&fp->f_lock); 
    if (fp->f_count == 1) { 
        pthread_mutex_unlock(&fp->f_lock); 
        pthread_mutex_lock(&hashlock); 
        pthread_mutex_lock(&fp->f_lock); 
        if (fp->f_count != 1) { 
            fp->f_count--; 
            pthread_mutex_unlock(&fp->f_lock); 
            pthread_mutex_unlock(&hashlock); 
            return; 
        } 
        idx = HASH(fp); 
        tfp = fh[idx]; 
        if (tfp == fp) { 
            fh[idx] = fp->f_next; 
        } 
        else { 
            while (tfp->f_next != fp) { 
                tfp = tfp->f_next; 
            } 
            tfp->f_next = fp->f_next; 
        } 
        pthread_mutex_unlock(&hashlock); 
        pthread_mutex_unlock(&fp->f_lock); 
        pthread_mutex_destroy(&fp->f_lock); 
        free(fp); 
    } 
    else { 
        fp->f_count--; 
        pthread_mutex_unlock(&fp->f_lock); 
    } 
}

上面樣例中加、減鎖太複雜,能夠使用雜湊列表鎖來保護結構引用計數,結構相互排斥量能夠用於保護foo結構中的其他不論什麼東西,例如以下:

#include <stdlib.h> 
#include <pthread.h> 

#define NHASH 29 
#define HASH(fp) (((unsigned long)fp)%NHASH) 

struct foo* fh[NHASH]; 
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER; 

struct foo { 
    int f_count; 
    pthread_mutex_t f_lock; 
    struct foo *f_next; 
    int f_id; 
}; 

struct foo* foo_alloc(void) 
{ 
    struct foo *fp; 
    int idx; 

    if ((fp = malloc(sizeof(struct foo))) != NULL) { 
        fp->f_count = 1; 
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { 
            free(fp); 
            return NULL; 
        } 
        idx = HASH(fp); 
        pthread_mutex_lock(&hashlock); 
        fp->f_next = fh[idx]; 
        fh[idx] = fp->f_next; 
        pthread_mutex_lock(&fp->f_lock); 
        pthread_mutex_unlock(&hashlock); 
    } 
    return fp; 
} 

void foo_hold(struct foo *fp) 
{ 
    pthread_mutex_lock(&hashlock); 
    fp->f_count++; 
    pthread_mutex_unlock(&hashlock); 
} 

struct foo* foo_find(int id) 
{ 
    struct foo *fp; 
    int idx; 
    idx = HASH(fp); 
    pthread_mutex_lock(&hashlock); 
    for (fp = fh[idx]; fp != NULL; fp = fp->f_next) { 
        if (fp->f_id == id) { 
            fp->f_count++; 
            break; 
        } 
    } 
    pthread_mutex_unlock(&hashlock); 
    return fp; 
} 


void foo_release(struct foo *fp) 
{ 
    struct foo *tfp; 
    int idx; 

    pthread_mutex_lock(&hashlock); 
    if (--(fp->f_count) == 0) { 
        idx = HASH(fp); 
        tfp = fh[idx]; 
        if (tfp == fp) { 
            fh[idx] = fp->f_next; 
        } 
        else { 
            while (tfp->f_next != fp) { 
                tfp = tfp->f_next; 
            } 
            tfp->f_next = fp->f_next; 
        } 
        pthread_mutex_unlock(&hashlock); 
        pthread_mutex_destroy(&fp->f_lock); 
        free(fp); 
    } 
    else { 
        pthread_mutex_unlock(&hashlock); 
    } 
}

假設鎖的粒度太粗,就會出現非常多執行緒堵塞等待同樣的鎖。源自併發性發改善微乎其微。假設鎖的粒度太細,那麼過多的鎖開銷會使系統效能收到影響。並且程式碼變得相當複雜。

作為一個程式猿。須要在滿足鎖需求的情況下,在程式碼複雜性和優化效能之間找好平衡點。

以下以一個簡單的樣例說明多執行緒共享資源的問題,主執行緒內啟動5個執行緒。這5個執行緒分別對初始值為0的全域性變數連續5次累加1、10、100、1000、10000。程式碼例如以下:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 

// pthread_mutex_t value_lock = PTHREAD_MUTEX_INITIALIZER; 
int value = 0; 

void* thread_func1(void *arg) 
{ 
    // pthread_mutex_lock(&value_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 1; 
        printf("thread 1: value = %d\n", value); 
    } 
    // pthread_mutex_unlock(&value_lock); 
    pthread_exit((void*)1); 
} 

void* thread_func2(void *arg) 
{ 
    // pthread_mutex_lock(&value_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 10; 
        printf("thread 2: value = %d\n", value); 
    } 
    // pthread_mutex_unlock(&value_lock); 
    pthread_exit((void*)2); 
} 

void* thread_func3(void *arg) 
{ 
    // pthread_mutex_lock(&value_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 100; 
        printf("thread 3: value = %d\n", value); 
    } 
    // pthread_mutex_unlock(&value_lock); 
    pthread_exit((void*)3); 
} 

void* thread_func4(void *arg) 
{ 
    // pthread_mutex_lock(&value_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 1000; 
        printf("thread 4: value = %d\n", value); 
    } 
    // pthread_mutex_unlock(&value_lock); 
    pthread_exit((void*)4); 
} 

void* thread_func5(void *arg) 
{ 
    // pthread_mutex_lock(&value_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 10000; 
        printf("thread 5: value = %d\n", value); 
    } 
    // pthread_mutex_unlock(&value_lock); 
    pthread_exit((void*)5); 
} 

int main(void) 
{ 
    int err; 
    pthread_t tid1, tid2, tid3, tid4, tid5; 

    err = pthread_create(&tid1, NULL, thread_func1, NULL); 
    if (0 != err) { 
        printf("can't create thread 1: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid2, NULL, thread_func2, NULL); 
    if (0 != err) { 
        printf("can't create thread 2: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid3, NULL, thread_func3, NULL); 
    if (0 != err) { 
        printf("can't create thread 3: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid4, NULL, thread_func4, NULL); 
    if (0 != err) { 
        printf("can't create thread 4: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid5, NULL, thread_func5, NULL); 
    if (0 != err) { 
        printf("can't create thread 5: %s\n", strerror(err)); 
        abort();         
    } 

    sleep(1); 
    printf("main thread end\n"); 

    exit(0); 
} 

執行結果例如以下(全然亂套了):

thread 2: value = 11 
thread 2: value = 121 
thread 2: value = 1131 
thread 2: value = 1141 
thread 2: value = 1151 
thread 3: value = 111 
thread 3: value = 1251 
thread 3: value = 1351 
thread 3: value = 1451 
thread 3: value = 1551 
thread 1: value = 1 
thread 1: value = 1552 
thread 1: value = 1553 
thread 1: value = 1554 
thread 1: value = 1555 
thread 4: value = 1121 
thread 4: value = 2555 
thread 4: value = 3555 
thread 4: value = 4555 
thread 4: value = 5555 
thread 5: value = 15555 
thread 5: value = 25555 
thread 5: value = 35555 
thread 5: value = 45555 
thread 5: value = 55555 
main thread end 

作為對照,我們使用相互排斥量。5個執行緒都要使用相互排斥量,要不結果也是不可預料的。把剛才程式碼的凝視開啟就可以。結果例如以下(預期結果bingo):

thread 1: value = 1 
thread 1: value = 2 
thread 1: value = 3 
thread 1: value = 4 
thread 1: value = 5 
thread 2: value = 15 
thread 2: value = 25 
thread 2: value = 35 
thread 2: value = 45 
thread 2: value = 55 
thread 3: value = 155 
thread 3: value = 255 
thread 3: value = 355 
thread 3: value = 455 
thread 3: value = 555 
thread 4: value = 1555 
thread 4: value = 2555 
thread 4: value = 3555 
thread 4: value = 4555 
thread 4: value = 5555 
thread 5: value = 15555 
thread 5: value = 25555 
thread 5: value = 35555 
thread 5: value = 45555 
thread 5: value = 55555 
main thread end 

以下演示一個死鎖的樣例,兩個執行緒分別訪問兩個全域性變數。這兩個全域性變數分別被兩個相互排斥量保護,但由於相互排斥量的使用順序不同。導致死鎖:

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <pthread.h> 

pthread_mutex_t value_lock = PTHREAD_MUTEX_INITIALIZER; 
pthread_mutex_t value2_lock = PTHREAD_MUTEX_INITIALIZER; 

int value = 0; 
int value2 = 0; 

void* thread_func1(void *arg) 
{ 
    pthread_mutex_lock(&value_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 1; 
        printf("thread 1: value = %d\n", value); 
    } 
    pthread_mutex_lock(&value2_lock); 
    count = 1; 
    while (count++ <= 5) { 
        value2 += 1; 
        printf("thread 1: value2= %d\n", value2); 
    } 
    pthread_mutex_unlock(&value_lock); 
    pthread_mutex_unlock(&value2_lock); 
    pthread_exit((void*)1); 
} 

void* thread_func2(void *arg) 
{ 
    pthread_mutex_lock(&value2_lock); 
    int count = 1; 
    while (count++ <= 5) { 
        value += 10; 
        printf("thread 2: value = %d\n", value); 
    } 
    pthread_mutex_lock(&value_lock); 
    count = 1; 
    while (count++ <= 5) { 
        value2 += 10; 
        printf("thread 2: value2= %d\n", value2); 
    } 
    pthread_mutex_unlock(&value_lock); 
    pthread_mutex_unlock(&value2_lock); 
    pthread_exit((void*)1); 
} 

int main(void) 
{ 
    int err; 
    pthread_t tid1, tid2; 

    err = pthread_create(&tid1, NULL, thread_func1, NULL); 
    if (0 != err) { 
        printf("can't create thread 1: %s\n", strerror(err)); 
        abort();         
    } 

    err = pthread_create(&tid2, NULL, thread_func2, NULL); 
    if (0 != err) { 
        printf("can't create thread 2: %s\n", strerror(err)); 
        abort();         
    } 

    sleep(1); 
    printf("main thread end\n"); 

    exit(0); 
} 

執行結果例如以下(value2沒有結果,假設相互排斥量使用順序同樣就正常了):

thread 1: value = 1 
thread 1: value = 12 
thread 1: value = 13 
thread 1: value = 14 
thread 1: value = 15 
thread 2: value = 11 
thread 2: value = 25 
thread 2: value = 35 
thread 2: value = 45 
thread 2: value = 55 
main thread end

(2)讀寫鎖
讀寫鎖rwlock,也叫共享-獨佔鎖,指的是一個執行緒獨佔寫鎖或者多個執行緒共享讀鎖。

讀鎖與寫鎖不能共存,當某個執行緒擁有寫鎖而還沒有解鎖前,其他執行緒試圖對這個鎖加鎖都會被堵塞;當某個執行緒擁有讀鎖而還沒有解鎖前,其他執行緒對這個鎖加讀鎖能夠成功,但加寫鎖時會堵塞,並且會堵塞後面的讀鎖,這樣能夠避免讀鎖長期佔用,而等待的寫鎖請求一直得不到滿足。

讀寫鎖經常使用於讀次數或讀頻率遠大於寫的情況。能夠提高併發性。

以下是讀寫鎖相關的幾個函式:

int pthread_rwlock_init (pthread_rwlock_t * restrict rwlock,
                const pthread_rwlockattr_t * restrict attr);
int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);
int pthread_rwlock_unwrlock (pthread_rwlock_t *rwlock);

上面幾個函式的使用方法同相互排斥量的使用方法一樣。以下以一個簡單的樣例說明,主執行緒中啟動10個執行緒。4個寫鎖。6個讀鎖:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

pthread_rwlock_t value_lock = PTHREAD_RWLOCK_INITIALIZER;

int value = 0;

void* thread_read(void *arg)
{
    pthread_rwlock_rdlock(&value_lock);
    printf("thread %d: value = %d\n", (int)arg, value);
    pthread_rwlock_unlock(&value_lock);
    pthread_exit(arg);
}

void* thread_write(void *arg)
{
    pthread_rwlock_wrlock(&value_lock);
    printf("thread %d: wrlock\n", (int)arg);
    int count = 1;
    while (count++ <= 10) {
        value += 1;
    }
    usleep(1000);
    pthread_rwlock_unlock(&value_lock);
    pthread_exit(arg);
}

int main(void)
{
    pthread_t tid;

    pthread_create(&tid, NULL, thread_read, (void*)1);
    pthread_create(&tid, NULL, thread_read, (void*)2);
    pthread_create(&tid, NULL, thread_write, (void*)3);
    pthread_create(&tid, NULL, thread_write, (void*)4);
    pthread_create(&tid, NULL, thread_read, (void*)5);
    pthread_create(&tid, NULL, thread_read, (void*)6);
    pthread_create(&tid, NULL, thread_write, (void*)7);
    pthread_create(&tid, NULL, thread_write, (void*)8);
    pthread_create(&tid, NULL, thread_read, (void*)9);
    pthread_create(&tid, NULL, thread_read, (void*)10);

    sleep(1);
    printf("main thread end\n");
    exit(0);
}

結果例如以下:

thread 2: value = 0
thread 1: value = 0
thread 3: wrlock
thread 4: wrlock
thread 5: value = 20
thread 6: value = 20
thread 7: wrlock
thread 8: wrlock
thread 9: value = 40
thread 10: value = 40
main thread end

假設把上面樣例中的寫鎖去掉後,結果就出乎意料了(明明更新了value,輸出的value卻還是0,亂套了,所以還是加上寫鎖為好):

thread 5: value = 0
thread 4: wrlock
thread 9: value = 10
thread 2: value = 0
thread 10: value = 10
thread 3: wrlock
thread 6: value = 0
thread 7: wrlock
thread 1: value = 0
thread 8: wrlock
main thread end

(3)條件變數
條件變數是執行緒可用的還有一種同步機制。

條件變數給多個執行緒提供了一個會合的場所。條件變數與相互排斥量一起使用時,同意執行緒以無競爭的方式等待特定的條件發生。條件本身是由相互排斥量保護的。執行緒在改變條件狀態前必須首先鎖住相互排斥量,其他執行緒在獲取相互排斥量之前不會察覺到這樣的改變。由於必須鎖定相互排斥量以後才幹計算條件。

先來看一下條件變數cond相關的函式:

int pthread_cond_init (pthread_cond_t *restrict cond,
                const pthread_condattr_t *restrict cond_attr);
int pthread_cond_destroy (pthread_cond_t *cond);
int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast (pthread_cond_t *cond);
int pthread_cond_wait (pthread_cond_t *restrict cond,
                pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait (pthread_cond_t *restrict cond,
                pthread_mutex_t *restrict mutex,
                const struct timespec *restrict abstime);

使用pthread_cond_wait等待條件變為真,假設在給定的時間內條件不能滿足,那麼會生成一個代表出錯碼的返回變數。**傳遞給pthread_cond_wait的相互排斥量對條件進行保護,呼叫者把鎖住的相互排斥量傳給函式。

函式把呼叫執行緒放到等待條件的執行緒列表上。然後對相互排斥量解鎖,這兩個操作是原子操作。這樣就關閉了條件檢查和執行緒進入休眠狀態等待條件改變這個兩個操作之間的時間通道。這樣執行緒就不會錯過條件的不論什麼變化。

pthread_cond_wait返回時,相互排斥量再次被鎖住。**pthread_cond_timedwait指定了等待的時間abstime,時間值是個絕對值。假設希望等待n分鐘。就須要把當前時間加上n分鐘再轉換到timespec結構,而不是把n分鐘直接轉換為timespec結構,時間可通過函式gettimeofday獲取。

假設時間值到了可是條件還是沒有出現,函式將又一次獲取相互排斥量然後返回錯誤ETIMEDOUT。

假設等待成功返回,執行緒須要又一次計算條件。由於其他的執行緒可能已經在執行並改變了條件。

有兩個函式能夠用於通知執行緒條件已經滿足。pthread_cond_signal函式將喚醒等待該條件的某個執行緒,而pthread_cond_broadcast函式將喚醒等待該條件的全部執行緒,呼叫這兩個函式也稱為向執行緒或條件傳送訊號,必須注意一定要在改變條件狀態後再給執行緒傳送訊號。

以下一個樣例說明相互排斥量與條件變數一起使用的使用方法:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

int value = 0;

pthread_cond_t value_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t value_mutex = PTHREAD_MUTEX_INITIALIZER;


void* thread_read(void *arg)
{
    pthread_mutex_lock(&value_mutex);
    pthread_cond_wait(&value_cond, &value_mutex);
    printf("thread %d: value = %d\n", (int)arg, value);
    pthread_mutex_unlock(&value_mutex);
    pthread_exit(arg);
}

void* thread_write(void *arg)
{
    pthread_mutex_lock(&value_mutex);
    printf("thread %d: mutex\n", (int)arg);
    value += 100;
    pthread_cond_signal(&value_cond);
    pthread_mutex_unlock(&value_mutex);
    pthread_exit(arg);
}

int main(void)
{
    pthread_t tid;

    pthread_create(&tid, NULL, thread_read, (void*)1);
    usleep(500 * 1000);
    pthread_create(&tid, NULL, thread_write, (void*)2);

    sleep(1);
    printf("main thread end\n");
    exit(0);
}

輸出結果:

thread 2: mutex
thread 1: value = 100
main thread end

上面樣例中,read執行緒和write執行緒之間增加了usleep,保證read執行緒先執行,但read執行緒中加鎖之後,在還沒有釋放鎖之前。write執行緒中的內容竟然也開始執行了,這是由於read執行緒中使用了條件變數,它會解鎖,write執行緒趁機執行。併傳送signal,read執行緒收到signal即wait返回時會又一次加鎖,然後完畢剩餘動作。

關於執行緒同步的幾個使用方法:相互排斥量就相當於一把鎖。有排它性,用於多個執行緒訪問共享資料時確保資料訪問的正確性,但前提是這些執行緒都必須使用同一個相互排斥量,要不也沒有效果,假設某個執行緒佔有這個鎖時,其他執行緒就不能佔用,從而保證了共享資料的安全;讀寫鎖可提高併發性。可同一時候擁有一個寫鎖或者多個讀鎖,當然系統也規定了讀鎖的上限;條件變數拉近了執行緒間的關係,但同一時候也要使用相互排斥量來保護條件變數。

相關文章