1. 程式人生 > 實用技巧 >Linux中的鎖,條件變數

Linux中的鎖,條件變數

為了保證多執行緒能夠正確的執行,於是有了鎖,鎖是為了解決執行緒對臨界資源的互斥訪問而生的機制。

互斥鎖

互斥鎖是最簡單的同步機制。程序要訪問加鎖的資源前先要獲得鎖,若鎖未被佔用,即獲得並佔用,用完釋放鎖。若執行緒訪問時鎖已被佔用,則由排程器阻塞該程序,直到鎖可用並且被排程使用。阻塞不佔用CPU。

由於鎖也是多執行緒的一部分,因此標頭檔案是 pthread.h,互斥鎖相關的API如下:

 pthread_mutex_t mutex;            /*建立鎖*/
 pthread_mutex_destroy(&mutex);    /*銷燬鎖*/
 pthread_mutex_init(
&mutex,NULL); /*建立後初始化才能使用*/ pthread_mutex_lock(&mutex); pthread_mutex_trylock(&mutex); pthread_mutex_timedlock(&mutex, &timeout); pthread_mutex_unlock(&mutex); /*釋放鎖*/

互斥鎖有三種獲得方式,阻塞呼叫 lock,如果呼叫時鎖已被佔用,執行緒就會被阻塞,加入到這個鎖的排隊佇列中。非阻塞呼叫 trylock,只是嘗試一下獲取鎖,如果沒人用就用,有人用了就不用。超時阻塞呼叫 timedlock,還是阻塞呼叫,但是設定一個最大阻塞時間,超過時間就不用了。

自旋鎖

自旋鎖的用法和互斥鎖一樣,只是在原理上稍有不同,導致它們的應用場景也大有不同。當鎖被佔用,另一個程序嘗試獲取鎖時,不想互斥鎖會將程序阻塞,自旋鎖是讓程序一直迴圈詢問鎖是否可用。這時CPU仍然一直被該程序佔用。相應的API使用如下。

 pthread_spinlock_t spin;        /*建立鎖*/
 pthread_spin_destroy(&spin);    /*銷燬鎖*/
 pthread_spin_init(&spin,NULL);  /*初始化鎖*/
 pthread_spin_lock(&spin);       /*獲取鎖*/
 pthread_spin_trylock(
&spin); pthread_spin_unlock(&spin); /*釋放鎖*/

對比:有的人說互斥鎖是sleep-wait模式,自旋鎖是busy-wait。我覺得很形象。

互斥鎖:鎖被佔用了?行吧,我也不是很急,我先小憩一下,並對排程器說:記得一會叫醒我。

自旋鎖:鎖被佔用了?我很急啊,快點快點啊,好了沒啊,好了沒啊…

所以互斥鎖用於可能阻塞很長時間的場景,自旋鎖用於阻塞時間很短的場景。

互斥鎖的系統開銷比自旋鎖大得多,需要進行系統的排程,執行緒上下文切換等,如果阻塞時間很短,那代價就有點高了。

讀寫鎖

讀寫鎖其實是一種特殊的自旋鎖。它把對共享資源的訪問者分成了讀者和寫者,寫是互斥的,一次只能有一個人寫;讀和寫也是互斥的,有人寫的時候就不能讀;但讀者之間不是互斥的,允許很多人一起讀。

讀寫鎖也叫共享-獨佔鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨佔模式鎖住的。寫獨佔--讀共享。

這是一種更加實用的鎖,提高了程式的併發效能。從實際應用角度出發,讀鎖的優先順序應該要低於寫鎖的優先順序。

遞迴鎖和非遞迴鎖

互斥鎖按照是否可遞迴的性質劃分的兩種鎖。遞迴鎖又叫做可重入鎖,指一個程序中可以多次進入鎖;非遞迴鎖又叫做不可重入鎖。

來看一個例子:

MutexLock mutex;      
void testa()  
{  
    mutex.lock();  
    do_sth();
    mutex.unlock();  
}     
void testb()  
{  
  mutex.lock();   
  testa();  
  mutex.unlock();   
}

如果mutex是非遞迴鎖,那麼呼叫testb()時就會造成死鎖。如果mutex是遞迴鎖,就允許這樣多次進鎖。

linux中的鎖預設為非遞迴鎖,可以設定為遞迴鎖。不過,不建議使用遞迴鎖,程式容易出問題。

條件變數

提到鎖還必須要介紹的是條件變數,通常條件變數和互斥鎖搭配使用。

條件變數讓程序能夠一直睡眠等待直到某種條件滿足才開始執行。我們直接看最經典的例子,生產者和消費者的例子。我們有一個產品佇列,假設是無窮大,消費者在佇列空的時候不能消費,需要等待,程式碼如下:

#include <pthread.h>
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void process_msg(void)
{
    struct msg *mp;
    for (;;) {
        pthread_mutex_lock(&qlock);
        while (workq == NULL)        /*a.*/
            pthread_cond_wait(&qready, &qlock);
        mp = workq;
        workq = mp->m_next;
        pthread_mutex_unlock(&qlock);
    }
}
void enqueue_msg(struct msg *mp)
{
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    pthread_cond_signal(&qready);   /*b.*/
}

pthread_cond_wait()會先將鎖釋放,然後該程序進入阻塞。

pthread_cond_signal()會通知wait的程序執行,結束阻塞。

關於條件變數值得探討的幾個問題:

1.有了互斥鎖為什麼還要條件變數?

沒有條件變數,只用互斥鎖當然也可以解決問題。比如上面的例子,不用條件變數,因為互斥鎖進入阻塞的程序不是一直阻塞的,是由排程器排程的,如果一直沒有生產,那麼消費者就會頻繁被叫醒,醒來卻發現什麼也沒有,又要去睡。這樣會消耗很多資源。對於這種可能會阻塞很久的應用場景,使用條件變數,系統會在條件滿足時才去喚醒程序,這樣程序切換隻發生一次,大大節省資源。所以條件變數是用於特殊場景的,為了提高資源利用率而產生的。

2.在上面程式碼中註釋a處,為什麼使用while而不是if?

這是一個很有意思的地方,也很巧妙。while和if不同之處在於,當wait結束時,仍然需要再迴圈一遍while條件,確保真的是有貨可以消費了。如果是if,就會直接執行下面的程式碼。有時候可能會出現特殊情況,比如中斷,故障,使得跳出了wait,這時再迴圈判斷一次,程式的健壯性非常好。

3.在程式碼註釋b處,傳送訊號和解鎖語句的前後順序有什麼要求呢?

如果傳送訊號語句放在之前,被喚醒的時候鎖還沒有釋放,是不是又會去sleep呢?在linux中作業系統的設計師考慮了這個問題,不會發生這樣的情況。

如果放在之後,會出現這樣的情況:釋放鎖的瞬間,正好有另一個消費者進入消費,這時通知原來的消費者醒來,執行一次while判斷,發現workq還是NULL,白醒來了。其實這樣也是可以的,就是允許了消費者程序之間的競爭。這時這個while就很精髓了,如果是if,程序就不能知道它的貨已經被搶了。

不過在linux中一般讓傳送訊號語句放在之前,不允許程序搶佔原來已經等了很久的程序的貨,這樣也比較公平。

*以上都是參照網路部落格整理的,歡迎網友勘誤,共同進步。