Posix線程編程指南(3) Posix線程編程指南(3)線程同步

這是一個關於Posix線程編程的專欄。作者在闡明概念的基礎上,將向您詳細講述Posix線程庫API。本文是第三篇將向您講述線程同步。

互斥鎖

 

儘管在Posix Thread中同樣可以使用IPC的信號量機制來實現互斥鎖mutex功能,但顯然semphore的功能過於強大了,在Posix Thread中定義了另外一套專門用於線程同步的mutex函數。

 

1 創建和銷燬

 

有兩種方法創建互斥鎖,靜態方式和動態方式。POSIX定義了一個宏PTHREAD_MUTEX_INITIALIZER來靜態初始化互斥鎖,方法如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; LinuxThreads實現中,pthread_mutex_t是一個結構,而PTHREAD_MUTEX_INITIALIZER則是一個結構常量。

 

動態方式是採用pthread_mutex_init()函數來初始化互斥鎖,API定義如下: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 其中mutexattr用於指定互斥鎖屬性(見下),如果爲NULL則使用缺省屬性。

 

pthread_mutex_destroy()用於註銷一個互斥鎖,API定義如下: int pthread_mutex_destroy(pthread_mutex_t *mutex) 銷燬一個互斥鎖即意味着釋放它所佔用的資源,且要求鎖當前處於開放狀態。由於在Linux中,互斥鎖並不佔用任何資源,因此LinuxThreads中的pthread_mutex_destroy()除了檢查鎖狀態以外(鎖定狀態則返回EBUSY)沒有其他動作。

 

2 互斥鎖屬性

 

互斥鎖的屬性在創建鎖的時候指定,在LinuxThreads實現中僅有一個鎖類型屬性,不同的鎖類型在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。當前(glibc2.2.3,linuxthreads0.9)有四個值可供選擇:

 

PTHREAD_MUTEX_TIMED_NP,這是缺省值,也就是普通鎖。當一個線程加鎖以後,其餘請求鎖的線程將形成一個等待隊列,並在解鎖後按優先級獲得鎖。這種鎖策略保證了資源分配的公平性。

PTHREAD_MUTEX_RECURSIVE_NP,嵌套鎖,允許同一個線程對同一個鎖成功獲得多次,並通過多次unlock解鎖。如果是不同線程請求,則在加鎖線程解鎖時重新競爭。

PTHREAD_MUTEX_ERRORCHECK_NP,檢錯鎖,如果同一個線程請求同一個鎖,則返回EDEADLK,否則與PTHREAD_MUTEX_TIMED_NP類型動作相同。這樣就保證當不允許多次加鎖時不會出現最簡單情況下的死鎖。

PTHREAD_MUTEX_ADAPTIVE_NP,適應鎖,動作最簡單的鎖類型,僅等待解鎖後重新競爭。

3 鎖操作

 

操作主要包括加鎖pthread_mutex_lock()、解鎖pthread_mutex_unlock()和測試加鎖 pthread_mutex_trylock()三個,不論哪種類型的鎖,都不可能被兩個不同的線程同時得到,而必須等待解鎖。對於普通鎖和適應鎖類型, 解鎖者可以是同進程內任何線程;而檢錯鎖則必須由加鎖者解鎖纔有效,否則返回EPERM;對於嵌套鎖,文檔和實現要求必須由加鎖者解鎖,但實驗結果表明並 沒有這種限制,這個不同目前還沒有得到解釋。在同一進程中的線程,如果加鎖後沒有解鎖,則任何其他線程都無法再獲得鎖。

 

int pthread_mutex_lock(pthread_mutex_t *mutex)int pthread_mutex_unlock(pthread_mutex_t *mutex)int pthread_mutex_trylock(pthread_mutex_t *mutex)

 

 

pthread_mutex_trylock()語義與pthread_mutex_lock()類似,不同的是在鎖已經被佔據時返回EBUSY而不是掛起等待。

 

4 其他

 

POSIX 線程鎖機制的Linux實現都不是取消點,因此,延遲取消類型的線程不會因收到取消信號而離開加鎖等待。值得注意的是,如果線程在加鎖後解鎖前被取消,鎖 將永遠保持鎖定狀態,因此如果在關鍵區段內有取消點存在,或者設置了異步取消類型,則必須在退出回調函數中解鎖。

 

這個鎖機制同時也不是異步信號安全的,也就是說,不應該在信號處理過程中使用互斥鎖,否則容易造成死鎖。

 

 

 

 

條件變量

條件變量是利用線程間共享的全局變量進行同步的一種機制,主要包括兩個動作:一個線程等待"條件變量的條件成立"而掛起;另一個線程使"條件成立"(給出條件成立信號)。爲了防止競爭,條件變量的使用總是和一個互斥鎖結合在一起。

 

1 創建和註銷

 

條件變量和互斥鎖一樣,都有靜態動態兩種創建方式,靜態方式使用PTHREAD_COND_INITIALIZER常量,如下:

pthread_cond_t cond=PTHREAD_COND_INITIALIZER

 

動態方式調用pthread_cond_init()函數,API定義如下:

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

 

儘管POSIX標準中爲條件變量定義了屬性,但在LinuxThreads中沒有實現,因此cond_attr值通常爲NULL,且被忽略。

 

註銷一個條件變量需要調用pthread_cond_destroy(),只有在沒有線程在該條件變量上等待的時候才能註銷這個條件變量,否則返回EBUSY。因爲Linux實現的條件變量沒有分配什麼資源,所以註銷動作只包括檢查是否有等待線程。API定義如下:

int pthread_cond_destroy(pthread_cond_t *cond)

 

2 等待和激發

 

 

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime)

 

 

等待條件有兩種方式:無條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式 如果在給定時刻前條件沒有滿足,則返回ETIMEOUT,結束等待,其中abstime以與time()系統調用相同意義的絕對時間形式出現,0表示格林 尼治時間197011000秒。

 

無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求pthread_cond_wait()(或 pthread_cond_timedwait(),下同)的競爭條件(Race Condition)。mutex互斥鎖必須是普通鎖(PTHREAD_MUTEX_TIMED_NP)或者適應鎖 PTHREAD_MUTEX_ADAPTIVE_NP),且在調用pthread_cond_wait()前必須由本線程加鎖 pthread_mutex_lock()),而在更新條件等待隊列以前,mutex保持鎖定狀態,並在線程掛起進入等待前解鎖。在條件滿足從而離開 pthread_cond_wait()之前,mutex將被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。

 

激發條件有兩種形式,pthread_cond_signal()激活一個等待該條件的線程,存在多個等待線程時按入隊順序激活其中一個;而pthread_cond_broadcast()則激活所有等待線程。

 

3 其他

 

pthread_cond_wait ()pthread_cond_timedwait()都被實現爲取消點,因此,在該處等待的線程將立即重新運行,在重新鎖定mutex後離開 pthread_cond_wait(),然後執行取消動作。也就是說如果pthread_cond_wait()被取消,mutex是保持鎖定狀態的, 因而需要定義退出回調函數來爲其解鎖。

 

以下示例集中演示了互斥鎖和條件變量的結合使用,以及取消對於條件等待動作的影響。在例子中,有兩個線程被啓動,並等待同一個條件變量,如果不使用退出回 調函數(見範例中的註釋部分),則tid2將在pthread_mutex_lock()處永久等待。如果使用回調函數,則tid2的條件等待及主線程的 條件激發都能正常工作。

 

#include <stdio.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t mutex;pthread_cond_t  cond;void * child1(void *arg){        pthread_cleanup_push(pthread_mutex_unlock,&mutex);  /* comment 1 */        while(1){                printf("thread 1 get running /n");        printf("thread 1 pthread_mutex_lock returns %d/n",pthread_mutex_lock(&mutex));        pthread_cond_wait(&cond,&mutex);                    printf("thread 1 condition applied/n");        pthread_mutex_unlock(&mutex);                    sleep(5);    }        pthread_cleanup_pop(0);     /* comment 2 */}void *child2(void *arg){        while(1){                sleep(3);               /* comment 3 */                printf("thread 2 get running./n");        printf("thread 2 pthread_mutex_lock returns %d/n",pthread_mutex_lock(&mutex));        pthread_cond_wait(&cond,&mutex);        printf("thread 2 condition applied/n");        pthread_mutex_unlock(&mutex);        sleep(1);        }}int main(void){        int tid1,tid2;        printf("hello, condition variable test/n");        pthread_mutex_init(&mutex,NULL);        pthread_cond_init(&cond,NULL);        pthread_create(&tid1,NULL,child1,NULL);        pthread_create(&tid2,NULL,child2,NULL);        do{        sleep(2);                   /* comment 4 */                pthread_cancel(tid1);       /* comment 5 */                sleep(2);                   /* comment 6 */        pthread_cond_signal(&cond);    }while(1);          sleep(100);        pthread_exit(0);}

 

 

如果不做註釋5pthread_cancel()動作,即使沒有那些sleep()延時操作,child1child2都能正常工作。註釋3和註釋4 的延遲使得child1有時間完成取消動作,從而使child2能在child1退出之後進入請求鎖操作。如果沒有註釋1和註釋2的回調函數定義,系統將 掛起在child2請求鎖的地方;而如果同時也不做註釋3和註釋4的延時,child2能在child1完成取消動作以前得到控制,從而順利執行申請鎖的 操作,但卻可能掛起在pthread_cond_wait()中,因爲其中也有申請mutex的操作。child1函數給出的是標準的條件變量的使用方 式:回調函數保護,等待條件前鎖定,pthread_cond_wait()返回後解鎖。

 

條件變量機制不是異步信號安全的,也就是說,在信號處理函數中調用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死鎖。

 

 

 

 

信號燈

信號燈與互斥鎖和條件變量的主要不同在於""的概念,燈亮則意味着資源可用,燈滅則意味着不可用。如果說後兩中同步方式側重於"等待"操作,即資源不可 用的話,信號燈機制則側重於點燈,即告知資源可用;沒有等待線程的解鎖或激發條件都是沒有意義的,而沒有等待燈亮的線程的點燈操作則有效,且能保持燈亮狀 態。當然,這樣的操作原語也意味着更多的開銷。

 

信號燈的應用除了燈亮/燈滅這種二元燈以外,也可以採用大於1的燈數,以表示資源數大於1,這時可以稱之爲多元燈。

 

1 創建和註銷

 

POSIX信號燈標準定義了有名信號燈和無名信號燈兩種,但LinuxThreads的實現僅有無名燈,同時有名燈除了總是可用於多進程之間以外,在使用上與無名燈並沒有很大的區別,因此下面僅就無名燈進行討論。

 

int sem_init(sem_t *sem, int pshared, unsigned int value)

這是創建信號燈的API,其中value爲信號燈的初值,pshared表示是否爲多進程共享而不僅僅是用於一個進程。LinuxThreads沒有實現 多進程共享信號燈,因此所有非0值的pshared輸入都將使sem_init()返回-1,且置errnoENOSYS。初始化好的信號燈由sem 量表徵,用於以下點燈、滅燈操作。

 

int sem_destroy(sem_t * sem)

被註銷的信號燈sem要求已沒有線程在等待該信號燈,否則返回-1,且置errnoEBUSY。除此之外,LinuxThreads的信號燈註銷函數不做其他動作。

 

2 點燈和滅燈

 

 

int sem_post(sem_t * sem)

 

點燈操作將信號燈值原子地加1,表示增加一個可訪問的資源。

 

int sem_wait(sem_t * sem)int sem_trywait(sem_t * sem)

 

sem_wait()爲等待燈亮操作,等待燈亮(信號燈值大於0),然後將信號燈原子地減1,並返回。sem_trywait()sem_wait()的非阻塞版,如果信號燈計數大於0,則原子地減1並返回0,否則立即返回-1errno置爲EAGAIN

3 獲取燈值

 

 

int sem_getvalue(sem_t * sem, int * sval)

 

讀取sem中的燈計數,存於*sval中,並返回0

4 其他

 

sem_wait()被實現爲取消點,而且在支持原子"比較且交換"指令的體系結構上,sem_post()是唯一能用於異步信號處理函數的POSIX異步信號安全的API

 

 

 

 

異步信號

由於LinuxThreads是在覈外使用核內輕量級進程實現的線程,所以基於內核的異步信號操作對於線程也是有效的。但同時,由於異步信號總是實際發往 某個進程,所以無法實現POSIX標準所要求的"信號到達某個進程,然後再由該進程將信號分發到所有沒有阻塞該信號的線程中"原語,而是隻能影響到其中一 個線程。

 

POSIX異步信號同時也是一個標準C庫提供的功能,主要包括信號集管理(sigemptyset()sigfillset()sigaddset ()sigdelset()sigismember()等)、信號處理函數安裝(sigaction())、信號阻塞控制(sigprocmask ())、被阻塞信號查詢(sigpending())、信號等待(sigsuspend())等,它們與發送信號的kill()等函數配合就能實現進程間 異步信號功能。LinuxThreads圍繞線程封裝了sigaction()raise(),本節集中討論LinuxThreads中擴展的異步信號 函數,包括pthread_sigmask()pthread_kill()sigwait()三個函數。毫無疑問,所有POSIX異步信號函數對於 線程都是可用的。

 

int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask)

設置線程的信號屏蔽碼,語義與sigprocmask()相同,但對不允許屏蔽的Cancel信號和不允許響應的Restart信號進行了保護。被屏蔽的信號保存在信號隊列中,可由sigpending()函數取出。

 

int pthread_kill(pthread_t thread, int signo)

thread號線程發送signo信號。實現中在通過thread線程號定位到對應進程號以後使用kill()系統調用完成發送。

 

int sigwait(const sigset_t *set, int *sig)

掛起線程,等待set中指定的信號之一到達,並將到達的信號存入*sig中。POSIX標準建議在調用sigwait()等待信號以前,進程中所有線程都 應屏蔽該信號,以保證僅有sigwait()的調用者獲得該信號,因此,對於需要等待同步的異步信號,總是應該在創建任何線程以前調用 pthread_sigmask()屏蔽該信號的處理。而且,調用sigwait()期間,原來附接在該信號上的信號處理函數不會被調用。

 

如果在等待期間接收到Cancel信號,則立即退出等待,也就是說sigwait()被實現爲取消點。

 

 

 

 

 

 

其他同步方式

 

除了上述討論的同步方式以外,其他很多進程間通信手段對於LinuxThreads也是可用的,比如基於文件系統的IPC(管道、UnixSocket等)、消息隊列(Sys.V或者Posix的)、System V的信號燈等。只有一點需要注意,LinuxThreads在覈內是作爲共享存儲區、共享文件系統屬性、共享信號處理、共享文件描述符的獨立進程看待的。

 

 

 

 

關於作者

 

 

   楊沙洲,男,現攻讀國防科大計算機學院計算機軟件方向博士學位。您可以通過電子郵件 [email protected]跟他聯繫。

 

 

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/hwz119/archive/2007/06/20/1658846.aspx

 

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