Linux 互斥鎖和條件變量,線程同步

與進程類似,線程也存在同步的問題,當多個控制線程共享相同的內存時,需要確保每個線程看到一致的數據視圖,如果每個線程使用的變量都是其他線程不會讀取或修改的(比如線程私有數據),就不會存在一致性問題。通常來說用戶可以使用互斥量(互斥鎖)或者的條件變量(條件鎖)的方式來解決線程的同步問題。
互斥鎖

       互斥鎖是一個簡單的鎖定命令,它可以用來鎖定共享資源使得其他線程無法訪問。互斥鎖具有以下特點:

       ·原子性:把一個互斥鎖定義爲一個原子操作,這意味着操作系統保證瞭如果一個線程鎖定了互斥鎖,則沒有其他線程可以在同一時間成功鎖定這個互斥量。

       ·唯一性:如果一個線程鎖定一個互斥量,在它接觸鎖定之前,沒有其他線程可以鎖定這個互斥量。

       ·非繁忙等待:如果一個線程已經鎖定了一個互斥鎖,第二個線程又試圖去鎖定這個互斥鎖,則第二個線程將被掛起(不佔用CPU資源),直到第一個線程解鎖,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥量。
鎖類型
互斥鎖類型 鎖類型     初始化方式           
普通鎖     PTHREAD_MUTEX_INITIALIZER           
嵌套鎖     PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP           
糾錯鎖     PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP           
自適應鎖     PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP           
鎖操作函數
創建與銷燬

       有兩種方法創建互斥鎖,靜態方式和動態方式。POSIX定義了一個宏PTHREAD_MUTEX_INITIALIZER來靜態初始化互斥鎖,方法如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

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

       動態方式是調用pthread_mutex_init函數,其中參數attr用於指定鎖的屬性。

pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr *attr);

       需要注意的是,不會出現有多個線程同時初始化同一個互斥鎖的情形,一個互斥鎖在使用期間一定不會被重新初始化。如果函數執行成功,則返回0,並將新創建的互斥鎖的ID值放到參數mutex中。如果執行失敗,那麼將返回一個錯誤編號。

       註銷鎖的函數是pthread_mutex_destroy。

pthread_mutex_destroy(pthread_mutex_t *mutex);

       需要注意的是,存儲互斥鎖的內存並不被釋放,如果pthread_mutex_destroy執行成功,則返回0,否則返回一個錯誤編號。另外,通過靜態創建的鎖不需要也不能使用這個函數註銷鎖。
鎖定與解鎖

       pthread_mutex_lock函數用於鎖定由參數mutex指向的互斥鎖。

int pthread_mutex_lock(pthread_mutex_t *mutex);

       如果mutex指向的鎖已經被鎖定,那麼當前調用鎖定函數的線程將阻塞直到互斥鎖被其他線程釋放(阻塞線程按照優先級等待)。當pthread_mutex_lock返回時,說明互斥鎖已經被當前線程成功鎖定。

       pthread_mutex_trylock函數用於嘗試給指定的互斥鎖加鎖。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

       該函數是pthread_mutex_lock的非阻塞版本。trylock在給一個互斥鎖加鎖時,如果互斥鎖已經被鎖定,那麼函數將返回錯誤而不會阻塞線程。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

       使用解鎖函數的前提是互斥鎖處於鎖定狀態,而且調用本函數的線程必須是給這個互斥鎖加鎖的線程才能解鎖(解鈴還須繫鈴人)。解鎖後,如果有其他線程在等待互斥鎖,那麼由調度程序決定哪個線程將獲得互斥鎖並脫離阻塞狀態。
條件變量

       使用互斥鎖雖然可以解決一些資源競爭的問題,但互斥鎖只有兩種狀態(加鎖和解鎖),這限制了互斥鎖的用途。

       條件變量(條件鎖)也可以解決線程同步和共享資源訪問的問題,條件變量是對互斥鎖的補充,它允許一個線程阻塞並等待另一個線程發送的信號,當收到信號時,阻塞的線程被喚醒並試圖鎖定與之相關的互斥鎖。
條件變量初始化

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

       pthread_cond_t cond = PTHREAD_COND_INITIALIZER

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

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

       條件變量的屬性由參數attr指定,如果參數attr爲NULL,那麼就使用默認的屬性設置。儘管POSIX標準中爲條件變量定義了屬性,但在LinuxThreads中沒有實現,因此cond_attr值通常爲NULL,且被忽略。多線程不能同時初始化一個條件變量,因爲這是原子操作。

       如果函數調用成功,則返回0,並將新創建的條件變量的ID放在參數cond中。
解除條件變量

int pthread_cond_destroy(pthread_cond_t *cond);

       調用destroy函數解除條件變量並不會釋放存儲條件變量的內存空間。
條件變量阻塞(等待)

    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 *abtime);

       等待有兩種方式:條件等待pthread_cond_wait()和計時等待pthread_cond_timedwait(),其中計時等待方式如果在給定時刻前條件沒有滿足,則返回ETIMEDOUT,結束等待,其中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()前的加鎖動作對應。阻塞時處於解鎖狀態。
激活

    int pthread_cond_signal(pthread_cond_t *cond);
    int pthread_cond_broadcast(pthread_cond_t *cond);

       pthread_cond_signal函數的作用是發送一個信號給另外一個正在處於阻塞等待狀態的線程,使其脫離阻塞狀態,繼續執行,如果沒有線程處在阻塞等待狀態,pthread_cond_signal也會成功返回。       

       共享變量的狀態改變必須遵守lock/unlock的規則:需要在同一互斥鎖的保護下使用pthread_cond_signal(即pthread_cond_wait必須放在pthread_mutex_lock和pthread_mutex_unlock之間)否則條件變量可以在對關聯條件變量的測試和pthread_cond_wait帶來的阻塞之間獲得信號,這將導致無限期的等待(死鎖)。因爲他要根據共享變量的狀態來決定是否要等待,所以爲了避免死鎖,必須要在lock/unlock隊中。

    共享變量的狀態改變必須遵守lock/unlock的規則:pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之間,也可以放在pthread_mutex_lock和pthread_mutex_unlock之後,但是各有優缺點。

       若爲前者,在某些線程的實現中,會造成等待線程從內核中喚醒(由於cond_signal)然後又回到內核空間(因爲cond_wait返回後會有原子加鎖的行爲),所以一來一回會有性能的問題(上下文切換)。詳細來說就是,當一個等待線程被喚醒的時候,它必須首先加鎖互斥量(參見pthread_cond_wait()執行步驟)。如果線程被喚醒而此時通知線程任然鎖住互斥量,則被喚醒線程會立刻阻塞在互斥量上,等待通知線程解鎖該互斥量,引起線程的上下文切換。當通知線程解鎖後,被喚醒線程繼續獲得鎖,再一次的引起上下文切換。這樣導致被喚醒線程不能順利加鎖,延長了加鎖時間,加重了系統不必要的負擔。但是在LinuxThreads或者NPTL裏面,就不會有這個問題,因爲在Linux 線程中,有兩個隊列,分別是cond_wait隊列和mutex_lock隊列, cond_signal只是讓線程從cond_wait隊列移到mutex_lock隊列,而不用返回到用戶空間,不會有性能的損耗,因此Linux推薦這種形式。

       而後者不會出現之前說的那個潛在的性能損耗,因爲在signal之前就已經釋放鎖了。但如果unlock和signal之前,有個低優先級的線程正在mutex上等待的話,那麼這個低優先級的線程就會搶佔高優先級的線程(cond_wait的線程)。而且,假設而這在上面的放中間的模式下是不會出現的。

       而對於pthread_cond_broadcast函數,它使所有由參數cond指向的條件變量阻塞的線程退出阻塞狀態,如果沒有阻塞線程,則函數無效。
---------------------

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