天天看點

互斥鎖pthread_mutex_t與pthread_cond_wait的使用

http://blog.chinaunix.net/uid-9543173-id-3579371.html

一、互斥鎖的使用

1、互斥鎖建立

有兩種方法建立互斥鎖,靜态方式和動态方式。

a)、POSIX定義了一個宏PTHREAD_MUTEX_INITIALIZER來靜态初始化互斥鎖,方法如下:

    pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

    在LinuxThreads實作中,pthread_mutex_t是一個結構,而PTHREAD_MUTEX_INITIALIZER則是一個結構常量。

b)、動态方式是采用pthread_mutex_init()函數來初始化互斥鎖,API定義如下:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)

其中mutexattr用于指定互斥鎖屬性(見下),如果為NULL則使用預設屬性。

2、pthread_mutex_destroy ()用于登出一個互斥鎖,API定義如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex)

銷毀一個互斥鎖即意味着釋放它所占用的資源,且要求鎖目前處于開放狀态。由于在Linux中,互斥鎖并不占用任何資源,是以LinuxThreads中的 pthread_mutex_destroy()除了檢查鎖狀态以外(鎖定狀态則傳回EBUSY)沒有其他動作。

3、互斥鎖屬性

   互斥鎖的屬性在建立鎖的時候指定,在LinuxThreads實作中僅有一個鎖類型屬性,不同的鎖類型在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。目前有四個值可供選擇:

  * PTHREAD_MUTEX_TIMED_NP,這是預設值,也就是普通鎖。當一個線程加鎖以後,其餘請求鎖的線程将形成一個等待隊列,并在解鎖後按優先級獲得鎖。這種鎖政策保證了資源配置設定的公平性。

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

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

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

4、鎖操作

   鎖操作主要包括加鎖 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而不是挂起等待。

看到這裡,讀者一定會好奇:Mutex的兩個基本操作lock和unlock是如何實作的呢?假設Mutex變量的值為1表示互斥鎖空閑,這時某個程序調用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經被某個線程獲得,其它線程再調用lock隻能挂起等待。那麼lock和unlock的僞代碼如下:

lock:  
    if(mutex > 0){  
        mutex = 0;  
        return 0;  
    } else  
        挂起等待;  
    goto lock;  
  
unlock:  
    mutex = 1;  
    喚醒等待Mutex的線程;  
    return 0;
           

unlock操作中喚醒等待線程的步驟可以有不同的實作,可以隻喚醒一個等待線程,也可以喚醒所有等待該Mutex的線程,然後讓被喚醒的這些線程去競争獲得這個Mutex,競争失敗的線程繼續挂起等待。

細心的讀者應該已經看出問題了:對Mutex變量的讀取、判斷和修改不是原子操作。如果兩個線程同時調用lock,這時Mutex是1,兩個線程都判斷mutex>0成立,然後其中一個線程置mutex=0,而另一個線程并不知道這一情況,也置mutex=0,于是兩個線程都以為自己獲得了鎖。

為了實作互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把寄存器和記憶體單元的資料相交換,由于隻有一條指令,保證了原子性,即使是多處理器平台,通路記憶體的總線周期也有先後,一個處理器上的交換指令執行時另一個處理器的交換指令隻能等待總線周期。現在我們把lock和unlock的僞代碼改一下(以x86的xchg指令為例):

lock:  
    movb $0, %al  
    xchgb %al, mutex  
    if(al寄存器的内容 > 0){  
        return 0;  
    } else  
        挂起等待;  
    goto lock;  
  
unlock:  
    movb $1, mutex  
    喚醒等待Mutex的線程;  
    return 0;  
           

      unlock中的釋放鎖操作同樣隻用一條指令實作,以保證它的原子性。

也許還有讀者好奇,Linux 中“挂起等待”和“喚醒等待線程”的操作如何實作?

每個Mutex有一個等待隊列,一個線程要在Mutex上挂起等待,首先在把自己加入等待隊列中,然後置線程狀态為睡眠,然後調用排程器函數切換到别的線程。一個線程要喚醒等待隊列中的其它線程,隻需從等待隊列中取出一項,把它的狀态從睡眠改為就緒,加入就緒隊列,那麼下次排程器函數執行時就有可能切換到被喚醒的線程。

二、線程間同步pthread_cond_wait

多線程的條件變量 條件變量是利用線程間共享的全局變量 進行同步的一種機制,主要包括兩個動作:一個線程等待"條件變量的條件成立"而挂起;另一個線程使"條件成立"(給出條件成立信号)。

      為了防止競争,條件變量 的使用總是和一個互斥鎖 結合在一起。

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()前必須由本線程加鎖(thread_mutex_lock()),而在更新條件等待隊列以前,mutex保持鎖定狀态,并線上程挂起進入等待前解鎖。在條件滿足進而離開pthread_cond_wait()之前,mutex将被重新加鎖,以與進入pthread_cond_wait()前的加鎖動作對應。 激發條件有兩種形式,pthread_cond_signal()激活一個等待該條件的線程,存在多個等待線程時按入隊順序激活其中一個;而pthread_cond_broadcast()則激活所有等待線程。

3、處理流程 pthread_cond_wait使用前必須由本線程調用pthread_mutex_lock加鎖

pthread_mutex_lock(&mtx);
pthread_cond_wait(&cond,&mtx);
pthread_mutex_unlock(&mtx);
           

pthread_cond_wait内部處理流程:解鎖--->阻塞休眠--->喚醒--->加鎖

示例:

現在來看一段典型的應用:看注釋即可。

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

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
struct node
{
    int n_number;
    struct node *n_next;
} *head=NULL;/*[thread_func]*/

/*釋放節點記憶體 */
static void cleanup_handler(void*arg)
{
    printf("Cleanup handler of second thread.\n");
    free(arg);
    (void)pthread_mutex_unlock(&mtx);
}

static void*thread_func(void*arg)
{
    struct node *p = NULL;
    pthread_cleanup_push(cleanup_handler, p);
    while (1)
    {
        pthread_mutex_lock(&mtx);
        //這個mutex_lock主要是用來保護wait等待臨界時期的情況,
        //當在wait為放入隊列時,這時,已經存在Head條件等待激活的條件,此時可能會漏掉這種處理
        //這個while要特别說明一下,單個pthread_cond_wait功能很完善,
        //為何這裡要有一個while (head == NULL)呢?因為pthread_cond_wait裡的線程可能會被意外喚醒,如果這個時候head != NULL,
        //則不是我們想要的情況。這個時候,應該讓線程繼續進入pthread_cond_wait
        while (head ==NULL)
        {
            pthread_cond_wait(&cond,&mtx);
            // pthread_cond_wait會先解除之前的pthread_mutex_lock鎖定的mtx,然後阻塞在等待隊列裡休眠,直到再次被喚醒
            //(大多數情況下是等待的條件成立而被喚醒,喚醒後,該程序會先鎖定先pthread_mutex_lock(&mtx);,
            // 再讀取資源 用這個流程是比較清楚的/*unlock-->block-->wait() return-->lock*/
        }
        p = head;
        head = head->n_next;
        printf("Got %d from front of queue\n", p->n_number);
        free(p);
        pthread_mutex_unlock(&mtx);//臨界區資料操作完畢,釋放互斥鎖
    }
    pthread_cleanup_pop(0);
    return 0;
    /*EC_CLEANUP_BGN (void)pthread_mutex_unlock(&mtx); EC_FLUSH("thread_func") return 1; EC_CLEANUP_END*/
}

int main(void)
{
    pthread_t tid;
    int i;
    struct node *p;
    pthread_create(&tid,NULL, thread_func,NULL);
    //子線程會一直等待資源,類似生産者和消費者,但是這裡的消費者可以是多個消費者,而不僅僅支援普通的單個消費者,這個模型雖然簡單,但是很強大 /*[tx6-main]*/
    for (i= 0; i< 10; i++)
    {
        p = (struct node*)malloc(sizeof(struct node));
        p->n_number= i;
        pthread_mutex_lock(&mtx);//需要操作head這個臨界資源,先加鎖,
        p->n_next= head;
        head = p;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mtx);//解鎖
        sleep(1);
    }
    
    printf("thread 1 wanna end the cancel thread 2.\n");
    pthread_cancel(tid);
    //關于pthread_cancel,有一點額外的說明,它是從外部終止子線程,
    //子線程會在最近的取消點,退出線程,而在我們的代碼裡,最近的取消點肯定就是pthread_cond_wait()了。
    pthread_join(tid,NULL);
    printf("All done -- exiting\n");
    return 0;/*[]*//*EC_CLEANUP_BGN return EXIT_FAILURE; EC_CLEANUP_END*/
}
           

http://blog.csdn.net/yusiguyuan/article/details/14160081