Linux多線程同步機制


http://blog.163.com/he_junwei/blog/static/19793764620141711130253/

 一、互斥鎖

盡管在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表示格林尼治時間1970年1月1日0時0分0秒。

   無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求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);
}

 

   如果不做注釋5的pthread_cancel()動作,即使沒有那些sleep()延時操作,child1和child2都能正常工作。注釋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,且置errno為ENOSYS。初始化好的信號燈由sem變量表征,用於以下點燈、滅燈操作。

int sem_destroy(sem_t * sem) 
被注銷的信號燈sem要求已沒有線程在等待該信號燈,否則返回-1,且置errno為EBUSY。除此之外,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,否則立即返回-1,errno置為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(管道、Unix域Socket等)、消息隊列(Sys.V或者Posix的)、System V的信號燈等。只有一點需要注意,LinuxThreads在核內是作為共享存儲區、共享文件系統屬性、共享信號處理、共享文件描述符的獨立進程看待的。

 

條件變量與互斥鎖、信號量的區別

1).互斥鎖必須總是由給它上鎖的線程解鎖,信號量的掛出即不必由執行過它的等待操作的同一進程執行。一個線程可以等待某個給定信號燈,而另一個線程可以掛出該信號燈。

2).互斥鎖要么鎖住,要么被解開(二值狀態,類型二值信號量)

3).由於信號量有一個與之關聯的狀態(它的計數值),信號量掛出操作總是被記住。然而當向一個條件變量發送信號時,如果沒有線程等待在該條件變量上,那么該信號將丟失。

4).互斥鎖是為了上鎖而設計的,條件變量是為了等待而設計的,信號燈即可用於上鎖,也可用於等待,因而可能導致更多的開銷和更高的復雜性。

 

線程同步之利器(1)——可遞歸鎖與非遞歸鎖  

http://blog.csdn.net/zouxinfox/article/details/5838861

 

概述

最常見的進程/線程的同步方法有互斥鎖(或稱互斥量Mutex),讀寫鎖(rdlock),條件變量(cond),信號量(Semophore)等。在Windows系統中,臨界區(Critical Section)和事件對象(Event)也是常用的同步方法。

 

簡單的說,互斥鎖保護了一個臨界區,在這個臨界區中,一次最多只能進入一個線程。如果有多個進程在同一個臨界區內活動,就有可能產生競態條件(race condition)導致錯誤。

 

讀寫鎖從廣義的邏輯上講,也可以認為是一種共享版的互斥鎖。如果對一個臨界區大部分是讀操作而只有少量的寫操作,讀寫鎖在一定程度上能夠降低線程互斥產生的代價。

 

條件變量允許線程以一種無競爭的方式等待某個條件的發生。當該條件沒有發生時,線程會一直處於休眠狀態。當被其它線程通知條件已經發生時,線程才會被喚醒從而繼續向下執行。條件變量是比較底層的同步原語,直接使用的情況不多,往往用於實現高層之間的線程同步。使用條件變量的一個經典的例子就是線程池(Thread Pool)了。

 

在學習操作系統的進程同步原理時,講的最多的就是信號量了。通過精心設計信號量的PV操作,可以實現很復雜的進程同步情況(例如經典的哲學家就餐問題和理發店問題)。而現實的程序設計中,卻極少有人使用信號量。能用信號量解決的問題似乎總能用其它更清晰更簡潔的設計手段去代替信號量。 

 

本系列文章的目的並不是為了講解這些同步方法應該如何使用(AUPE的書已經足夠清楚了)。更多的是講解很容易被人忽略的一些關於鎖的概念,以及比較經典的使用與設計方法。文章會涉及到遞歸鎖與非遞歸鎖(recursive mutex和non-recursive mutex),區域鎖(Scoped Lock),策略鎖(Strategized Locking),讀寫鎖與條件變量,雙重檢測鎖(DCL),鎖無關的數據結構(Locking free),自旋鎖等等內容,希望能夠拋磚引玉。

那么我們就先從遞歸鎖與非遞歸鎖說開去吧:)

 

1 可遞歸鎖與非遞歸鎖
1.1 概念

    在所有的線程同步方法中,恐怕互斥鎖(mutex)的出場率遠遠高於其它方法。互斥鎖的理解和基本使用方法都很容易,這里不做更多介紹了。

Mutex可以分為遞歸鎖(recursive mutex)和非遞歸鎖(non-recursive mutex)。可遞歸鎖也可稱為可重入鎖(reentrant mutex),非遞歸鎖又叫不可重入鎖(non-reentrant mutex)。

二者唯一的區別是,同一個線程可以多次獲取同一個遞歸鎖,不會產生死鎖。而如果一個線程多次獲取同一個非遞歸鎖,則會產生死鎖。

Windows下的Mutex和Critical Section是可遞歸的。Linux下的pthread_mutex_t鎖默認是非遞歸的。可以顯示的設置PTHREAD_MUTEX_RECURSIVE屬性,將pthread_mutex_t設為遞歸鎖。

在大部分介紹如何使用互斥量的文章和書中,這兩個概念常常被忽略或者輕描淡寫,造成很多人壓根就不知道這個概念。但是如果將這兩種鎖誤用,很可能會造成程序的死鎖。請看下面的程序。

 

 

[cpp]  view plain copy
 
  1. MutexLock mutex;  
  2.   
  3. void foo()  
  4. {  
  5.     mutex.lock();  
  6.     // do something  
  7.     mutex.unlock();  
  8. }  
  9.   
  10. void bar()  
  11. {  
  12.     mutex.lock();  
  13.     // do something  
  14.     foo();  
  15.     mutex.unlock();   
  16. }  

 

foo函數和bar函數都獲取了同一個鎖,而bar函數又會調用foo函數。如果MutexLock鎖是個非遞歸鎖,則這個程序會立即死鎖。因此在為一段程序加鎖時要格外小心,否則很容易因為這種調用關系而造成死鎖。

    不要存在僥幸心理,覺得這種情況是很少出現的。當代碼復雜到一定程度,被多個人維護,調用關系錯綜復雜時,程序中很容易犯這樣的錯誤。慶幸的是,這種原因造成的死鎖很容易被排除。

    但是這並不意味着應該用遞歸鎖去代替非遞歸鎖。遞歸鎖用起來固然簡單,但往往會隱藏某些代碼問題。比如調用函數和被調用函數以為自己拿到了鎖,都在修改同一個對象,這時就很容易出現問題。因此在能使用非遞歸鎖的情況下,應該盡量使用非遞歸鎖,因為死鎖相對來說,更容易通過調試發現。程序設計如果有問題,應該暴露的越早越好。

 

1.2 如何避免

       為了避免上述情況造成的死鎖,AUPE v2一書在第12章提出了一種設計方法。即如果一個函數既有可能在已加鎖的情況下使用,也有可能在未加鎖的情況下使用,往往將這個函數拆成兩個版本---加鎖版本和不加鎖版本(添加nolock后綴)。

   例如將foo()函數拆成兩個函數。

 

[cpp]  view plain copy
 
  1. // 不加鎖版本  
  2. void foo_nolock()  
  3. {  
  4.     // do something  
  5. }  
  6. // 加鎖版本  
  7. void fun()  
  8. {  
  9.     mutex.lock();  
  10.     foo_nolock();  
  11.     mutex.unlock();  
  12. }  

 

   為了接口的將來的擴展性,可以將bar()函數用同樣方法拆成bar_withou_lock()函數和bar()函數。

   在Douglas C. Schmidt(ACE框架的主要編寫者)的“Strategized Locking, Thread-safe Interface, and Scoped Locking”論文中,提出了一個基於C++的線程安全接口模式(Thread-safe interface pattern),與AUPE的方法有異曲同工之妙。即在設計接口的時候,每個函數也被拆成兩個函數,沒有使用鎖的函數是private或者protected類型,使用鎖的的函數是public類型。接口如下:

 

[cpp]  view plain copy
 
  1. class T  
  2. {  
  3. public:  
  4.     foo(); //加鎖  
  5.     bar(); //加鎖  
  6. private:  
  7.     foo_nolock();  
  8.     bar_nolock();  
  9. }  

 

作為對外接口的public函數只能調用無鎖的私有變量函數,而不能互相調用。在函數具體實現上,這兩種方法基本是一樣的。

   上面講的兩種方法在通常情況下是沒問題的,可以有效的避免死鎖。但是有些復雜的回調情況下,則必須使用遞歸鎖。比如foo函數調用了外部庫的函數,而外部庫的函數又回調了bar()函數,此時必須使用遞歸鎖,否則仍然會死鎖。AUPE 一書在第十二章就舉了一個必須使用遞歸鎖的程序例子。

1.3 讀寫鎖的遞歸性

    讀寫鎖(例如Linux中的pthread_rwlock_t)提供了一個比互斥鎖更高級別的並發訪問。讀寫鎖的實現往往是比互斥鎖要復雜的,因此開銷通常也大於互斥鎖。在我的Linux機器上實驗發現,單純的寫鎖的時間開銷差不多是互斥鎖十倍左右。

在系統不支持讀寫鎖時,有時需要自己來實現,通常是用條件變量加讀寫計數器實現的。有時可以根據實際情況,實現讀者優先或者寫者優先的讀寫鎖。

   讀寫鎖的優勢往往展現在讀操作很頻繁,而寫操作較少的情況下。如果寫操作的次數多於讀操作,並且寫操作的時間都很短,則程序很大部分的開銷都花在了讀寫鎖上,這時反而用互斥鎖效率會更高些。

   相信很多同學學習了讀寫鎖的基本使用方法后,都寫過下面這樣的程序(Linux下實現)。

 

[cpp]  view plain copy
 
  1. #include <pthread.h>  
  2. int main()  
  3. {  
  4.     pthread_rwlock_t rwl;  
  5.     pthread_rwlock_rdlock(&rwl);  
  6.     pthread_rwlock_wrlock(&rwl);  
  7.     pthread_rwlock_unlock(&rwl);  
  8.     pthread_rwlock_unlock(&rwl);  
  9.     return -1;  
  10. }  
  11.   
  12. /*程序2*/  
  13. #include <pthread.h>  
  14. int main()  
  15. {  
  16.     pthread_rwlock_t rwl;  
  17.     pthread_rwlock_wrlock(&rwl);  
  18.     pthread_rwlock_rdlock(&rwl);  
  19.     pthread_rwlock_unlock(&rwl);  
  20.     pthread_rwlock_unlock(&rwl);  
  21.     return -1;  
  22. }  

 

    你會很疑惑的發現,程序1先加讀鎖,后加寫鎖,按理來說應該阻塞,但程序卻能順利執行。而程序2卻發生了阻塞。 

    更近一步,你能說出執行下面的程序3和程序4會發生什么嗎?

 

[cpp]  view plain copy
 
  1. /*程序3*/  
  2. #include <pthread.h>  
  3. int main()  
  4. {  
  5.     pthread_rwlock_t rwl;  
  6.     pthread_rwlock_rdlock(&rwl);  
  7.     pthread_rwlock_rdlock(&rwl);  
  8.     pthread_rwlock_unlock(&rwl);  
  9.     pthread_rwlock_unlock(&rwl);  
  10.     return -1;  
  11. }  
  12. /*程序4*/  
  13. #include <pthread.h>  
  14. int main()  
  15. {  
  16.     pthread_rwlock_t rwl;  
  17.     pthread_rwlock_wrlock(&rwl);  
  18.     pthread_rwlock_wrlock(&rwl);  
  19.     pthread_rwlock_unlock(&rwl);  
  20.     pthread_rwlock_unlock(&rwl);  
  21.     return -1;  
  22. }  

 

POSIX標准中,如果一個線程先獲得寫鎖,又獲得讀鎖,則結果是無法預測的。這就是為什么程序1的運行出人所料。需要注意的是,讀鎖是遞歸鎖(即可重入),寫鎖是非遞歸鎖(即不可重入)。因此程序3不會死鎖,而程序4會一直阻塞。 

讀寫鎖是否可以遞歸會可能隨着平台的不同而不同,因此為了避免混淆,建議在不清楚的情況下盡量避免在同一個線程下混用讀鎖和寫鎖。 

   在系統不支持遞歸鎖,而又必須要使用時,就需要自己構造一個遞歸鎖。通常,遞歸鎖是在非遞歸互斥鎖加引用計數器來實現的。簡單的說,在加鎖前,先判斷上一個加鎖的線程和當前加鎖的線程是否為同一個。如果是同一個線程,則僅僅引用計數器加1。如果不是的話,則引用計數器設為1,則記錄當前線程號,並加鎖。一個例子可以看這里。需要注意的是,如果自己想寫一個遞歸鎖作為公用庫使用,就需要考慮更多的異常情況和錯誤處理,讓代碼更健壯一些。

 

下一節,我會介紹一下平時很常見的加鎖手段---區域鎖(Scoped Locking)技術。

什么是區域鎖

確切的說,區域鎖(Scoped locking)不是一種鎖的類型,而是一種鎖的使用模式(pattern)。這個名詞是Douglas C. Schmidt於1998年在其論文Scoped Locking提出,並在ACE框架里面使用。但作為一種設計思想,這種鎖模式應該在更早之前就被業界廣泛使用了。

區域鎖實際上是RAII模式在鎖上面的具體應用。RAII(Resource Acquisition Is Initialization)翻譯成中文叫“資源獲取即初始化”,最早是由C++的發明者 Bjarne Stroustrup為解決C++中資源分配與銷毀問題而提出的。RAII的基本含義就是:C++中的資源(例如內存,文件句柄等等)應該由對象來管理,資源在對象的構造函數中初始化,並在對象的析構函數中被釋放。STL中的智能指針就是RAII的一個具體應用。RAII在C++中使用如此廣泛,甚至可以說,不會RAII的裁縫不是一個好程序員。

問題提出

先看看下面這段程序,Cache是一個可能被多個線程訪問的緩存類,update函數將字符串value插入到緩存中,如果插入失敗,則返回-1。

 

[cpp]  view plain copy
 
  1. Cache *cache = new Cache;  
  2. ThreadMutex mutex;  
  3. int update(string value)  
  4. {  
  5.     mutex.lock();  
  6.     if (cache == NULL)  
  7.     {  
  8.         mutex.unlock();  
  9.         return -1;  
  10.     }  
  11.     If (cache.insert(value) == -1)  
  12.     {  
  13.     mutex.unlock();  
  14.         return -1;  
  15.     }  
  16.     mutex.unlock();  
  17.     return 0;  
  18. }  

 

 

從這個程序中可以看出,為了保證程序不會死鎖,每次函數需要return時,都要需要調用unlock函數來釋放鎖。不僅如此,假設cache.insert(value)函數內部突然拋出了異常,程序會自動退出,鎖仍然能不會釋放。實際上,不僅僅是return,程序中的goto, continue, break語句,以及未處理的異常,都需要程序員檢查鎖是否需要顯示釋放,這樣的程序是極易出錯的。

同樣的道理,不僅僅是鎖,C++中的資源釋放都面臨同樣的問題。例如前一陣我在閱讀wget源碼的時候,就發現雖然一共只有2萬行C代碼,但是至少有5處以上的return語句忘記釋放內存,因此造成了內存泄露。

區域鎖的實現

但是自從C++有了有可愛的RAII設計思想,資源釋放問題就簡單了很多。區域鎖就是把鎖封裝到一個對象里面。鎖的初始化放到構造函數,鎖的釋放放到析構函數。這樣當鎖離開作用域時,析構函數會自動釋放鎖。即使運行時拋出異常,由於析構函數仍然會自動運行,所以鎖仍然能自動釋放。一個典型的區域鎖

 

[cpp]  view plain copy
 
  1. class Thread_Mutex_Guard   
  2. {  
  3. public:  
  4.     Thread_Mutex_Guard (Thread_Mutex &lock)  
  5.     : lock_ (&lock)   
  6.     {   
  7.         // 如果加鎖失敗,則返回-1  
  8.         owner_= lock_->lock();   
  9.     }  
  10.   
  11.     ~Thread_Mutex_Guard (void)   
  12.     {  
  13.         // 如果鎖獲取失敗,就不釋放  
  14.         if (owner_ != -1)  
  15.             lock_->unlock ();  
  16.     }  
  17. private:  
  18.     Thread_Mutex *lock_;  
  19.     int owner_;  
  20. };  

 

將策略鎖應用到前面的update函數如下

 

[cpp]  view plain copy
 
  1. Cache *cache = new Cache;  
  2. ThreadMutex mutex;  
  3. int update(string value)  
  4. {  
  5.     Thread_Mutex_Guard (mutex)  
  6.     if (cache == NULL)  
  7.     {  
  8.         return -1;  
  9.     }  
  10.     If (cache.insert(value) == -1)  
  11.     {  
  12.         return -1;  
  13.     }  
  14.     return 0;  
  15. }  

 

基本的區域鎖就這么簡單。如果覺得這樣鎖的力度太大,可以用中括號來限定鎖的作用區域,這樣就能控制鎖的力度。如下

 

[cpp]  view plain copy
 
  1. {  
  2.     Thread_Mutex_Guard guard (&lock);  
  3.     ...............  
  4.     // 離開作用域,鎖自動釋放  
  5. }  

 

區域鎖的改進方案

上面設計的區域鎖一個缺點是靈活行,除非離開作用域,否則不能夠顯式釋放鎖。如果為一個區域鎖增加顯式釋放接口,一個最突出的問題是有可能會造成鎖的二次釋放,從而引發程序錯誤。

例如

 

[cpp]  view plain copy
 
  1. {  
  2.     Thread_Mutex_Guard guard (&lock);  
  3.     If (…)  
  4.     {  
  5.         //顯式釋放(第一次釋放)  
  6.         guard.release();  
  7.         // 自動釋放(第二次釋放)  
  8.         return -1;  
  9.     }  
  10. }  

 

為了避免二次釋放鎖引發的錯誤,區域鎖需要保證只能夠鎖釋放一次。一個改進的區域鎖如下:

 

[cpp]  view plain copy
 
  1. class Thread_Mutex_Guard   
  2. {  
  3. public:  
  4.     Thread_Mutex_Guard (Thread_Mutex &lock)  
  5.     : lock_ (&lock)   
  6.     {   
  7.         acquire();   
  8.     }  
  9.     int acquire()  
  10.     {  
  11.         // 加鎖失敗,返回-1  
  12.         owner_= lock_->lock();  
  13.         return owner;  
  14.     }  
  15.     ~Thread_Mutex_Guard (void)   
  16.     {  
  17.         release();  
  18.     }  
  19.     int release()  
  20.     {  
  21.         // 第一次釋放  
  22.         if (owner_ !=  -1)  
  23.         {  
  24.             owner = -1;  
  25.             return lock_->unlock ();  
  26.         }  
  27.         // 第二次釋放  
  28.         return 0;  
  29.     }  
  30. private:  
  31.     Thread_Mutex *lock_;  
  32.     int owner_;  
  33. };  

 

可以看出,這種方案在加鎖失敗或者鎖的多次釋放情況下,不會引起程序的錯誤。

缺點:

區域鎖固然好使,但也有不可避免的一些缺點

(1) 對於非遞歸鎖,有可能因為重復加鎖而造成死鎖。

(2) 線程的強制終止或者退出,會造成區域鎖不會自動釋放。應該盡量避免這種情形,或者使用一些特殊的錯誤處理設計來確保鎖會釋放。

(3) 編譯器會產生警告說有變量只定義但沒有使用。有些編譯器選項甚至會讓有警告的程序無法編譯通過。在ACE中,為了避免這種情況,作者定義了一個宏如下

#define UNUSED_ARG(arg) { if (&arg) /* null */; }

使用如下:

 

[cpp]  view plain copy
 
  1. Thread_Mutex_Guard guard (lock_);  
  2. UNUSED_ARG (guard);  

 

這樣編譯器就不會再警告了。

擴展閱讀:小技巧--如何在C++中實現Java的synchronized關鍵字

借助於區域鎖的思想,再定義一個synchronized宏,可以在C++中實現類似Java中的synchronized關鍵字功能。鏈接:http://www.codeproject.com/KB/threads/cppsyncstm.aspx

【轉載】死鎖原因及解決、避免辦法  

死鎖的條件
     互斥條件(Mutual exclusion)     :資源不能被共享,只能由一個進程使用。
     請求與保持條件(Hold and wait)進程已獲得了一些資源,但因請求其它資源被阻塞時,對已獲得的資源保持不放
     不可搶占條件(No pre-emption)    :有些系統資源是不可搶占的,當某個進程已獲得這種資源后,系統不能強行收回,只能由進程使用完時自己釋放

     循環等待條件(Circular wait)      :若干個進程形成環形鏈,每個都占用對方申請的下一個資源

處理死鎖的策略

1、忽略該問題。例如鴕鳥算法

 2、檢測死鎖並且恢復。
3、仔細地對資源進行動態分配,以避免死鎖。
4、通過破除死鎖四個必要條件之一,來防止死鎖產生。

鴕鳥算法:

該算法可以應用在極少發生死鎖的的情況下。為什么叫鴕鳥算法呢,因為傳說中鴕鳥看到危險就把頭埋在地底下,可能鴕鳥覺得看不到危險也就沒危險了吧。跟掩耳盜鈴有點像。

銀行家算法:

        所謂銀行家算法,是指在分配資源之前先看清楚,資源分配后是否會導致系統死鎖。如果會死鎖,則不分配,否則就分配。

按照銀行家算法的思想,當進程請求資源時,系統將按如下原則分配系統資源:

(1) 當一個進程對資源的最大需求量不超過系統中的資源數時可以接納該進程。

(2) 進程可以分期請求資源,當請求的總數不能超過最大需求量。

(3) 當系統現有的資源不能滿足進程尚需資源數時,對進程的請求可以推遲分配,但總能使進程在有限的時間里得到資源。

(4) 當系統現有的資源能滿足進程尚需資源數時,必須測試系統現存的資源能否滿足該進程尚需的最大資源數,若能滿足則按當前的申請量分配資源,否則也要推遲分配。

解決死鎖的策略

對待死鎖的策略主要有:

(1) 死鎖預防:破壞導致死鎖必要條件中的任意一個就可以預防死鎖。例如,要求用戶申請資源時一次性申請所需要的全部資源,這就破壞了保持和等待條件;將資源分層,得到上一層資源后,才能夠申請下一層資源,它破壞了環路等待條件。預防通常會降低系統的效率。

(2) 死鎖避免:避免是指進程在每次申請資源時判斷這些操作是否安全,例如,使用銀行家算法。死鎖避免算法的執行會增加系統的開銷。

(3) 死鎖檢測:死鎖預防和避免都是事前措施,而死鎖的檢測則是判斷系統是否處於死鎖狀態,如果是,則執行死鎖解除策略。

(4) 死鎖解除:這是與死鎖檢測結合使用的,它使用的方式就是剝奪。即將某進程所擁有的資源強行收回,分配給其他的進程。

 

死鎖的避免:
死鎖的預防是通過破壞產生條件來阻止死鎖的產生,但這種方法破壞了系統的並行性和並發性。
死鎖產生的前三個條件是死鎖產生的必要條件,也就是說要產生死鎖必須具備的條件,而不是存在這3個條件就一定產生死鎖,那么只要在邏輯上回避了第四個條件就可以避免死鎖。
避免死鎖采用的是允許前三個條件存在,但通過合理的資源分配算法來確保永遠不會形成環形等待的封閉進程鏈,從而避免死鎖。該方法支持多個進程的並行執行,為了避免死鎖,系統動態的確定是否分配一個資源給請求的進程。方法如下:
1.如果一個進程的當前請求的資源會導致死鎖,系統拒絕啟動該進程;
2.如果一個資源的分配會導致下一步的死鎖,系統就拒絕本次的分配;
顯然要避免死鎖,必須事先知道系統擁有的資源數量及其屬性

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM