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中一般讓傳送訊號語句放在之前,不允許程序搶佔原來已經等了很久的程序的貨,這樣也比較公平。
*以上都是參照網路部落格整理的,歡迎網友勘誤,共同進步。