在有了進程和線程的模型之后,一個很大的問題就擺在眼前:進程和線程的執行順序是不可預知的,那么,如何使得兩個進程按照我們想要的順序執行,從而得出正確的結果呢?
競爭條件:兩個或者多個進程讀寫某些共享數據,最后的結果依賴於進程運行的精確時序。
臨界區:把對共享內存進行訪問的程序片段稱作臨界區。如果能使兩個進程不可能同時處於臨界區內,就能夠避免競爭。
先引入一個經典的進程同步問題:生產者-消費者問題。
生產者-消費者問題:有一個緩沖區,一個(或多個)進程在生產某種產品,它生產的東西會放入緩沖區內;一個(或多個)進程在消費產品,它會從緩沖區內取走產品。當緩沖區滿時,生產者應當暫時停止生產;當緩沖區為空時,消費者應當暫時停止消費。
很顯然,這個問題用簡單的判斷緩沖區是否為0或N是無法解決的。如果在消費者判斷緩沖區為0時,恰好遇到了進程切換,生產者進程開始運行,此時應當喚醒消費者,然而這個信號丟失了,因為切換到消費者才進行了睡眠。這時,生產者會不斷運行,直到緩沖區滿,兩個進程全部睡眠,造成了死鎖。代碼如下:
#define N 1000 int count=0; void producer(void) { int item; while(TRUE) { item=produce_item(); if(count==N) sleep();//一段時間后,緩沖區滿,生產者進程也睡眠了 insert_item(item); count=count+1; if(count==1) wakeup(consumer);//設想判斷條件成立時,切換了進程,再次切回時,喚醒消費者進程,然而消費者進程此時沒有睡眠,信號丟失 } } void consumer(void) { int item; while(TRUE) { if(count==0) sleep();//第一次count=1,消費者進程不會睡眠;第二次確實睡眠了 item=remove_item(); count=count-1;//此時緩沖區確實為空了 if(count==N-1) wakeup(producer); consume_item(item); } }
一、信號量
信號量是一種數據結構,可以理解為一個用來計數的整數和一個隊列。整數用來記錄喚醒次數,而隊列被用來記錄因為該信號量而阻塞的進程。
信號量只支持兩種操作:P/V操作。
P操作,可以理解為測試並減一。P(signal1),如果signal1大於0,那么把它減一,進程繼續執行;如果signal為0,那么執行P操作的進程將會被阻塞,從而變為阻塞態,添加到因為signal1信號而阻塞的進程隊列中。
V操作,可以理解為+1並喚醒。V(signal1)后,如果signal1本來就大於0,那么執行+1;如果有進程在該信號量上被阻塞,那么從隊列中根據某種策略選擇一個進程喚醒。如果多個進程在該信號量上阻塞,那么V操作后,signal1仍然可能為負數。
需要注意的是,P/V操作均應當是原子操作,即作為一個整體執行而不會被打斷。
有了信號量,我們再來看生產者-消費者問題:
#define N 1000 typedef int semaphore; semaphore mutex=1;//控制對臨界區的訪問,其實就是互斥量 semaphore empty=N;//表示空槽的數量 semaphore full=0;//填滿的槽的數量 int count=0; void producer(void) { int item; while(TRUE) { item=produce_item(); down(&empty); down(&mutex);//要改變共享區(緩沖區),加鎖 insert_item(item); up(&mutex);//解鎖 up(&full); } } void consumer(void) { int item; while(TRUE) { down(&full); down(&mutex); item=remove_item(); up(&mutex); up(&empty); consume_item(item); } }
有了信號量,這個問題就好解決多了:用信號量full、empty來表示已用和未用的數量,這樣不管是滿了還是空了,都不會造成死鎖的問題。mutex的操作就是我們接下來要介紹的互斥鎖。
二、互斥鎖
互斥量其實可以理解為一個簡化的信號量,它只有兩種狀態:0和1。互斥鎖是用來解決進程(線程)互斥問題的。所謂進程互斥,就是兩個進程實際上是一種互斥的關系,兩者不能同時訪問共享資源。
互斥量和信號量原理比較類似,一旦一個線程獲得了鎖,那么其它線程就無法訪問共享資源,從而被阻塞,直到該線程交還出了鎖的所有權,另外一個線程才能獲得鎖。
互斥鎖的例子就不再給出,上面程序中已經有了,下面的程序中也會出現。
三、條件變量
條件變量是另外一種同步機制,可以用於線程和管程中的進程互斥。通常與互斥量一起使用。
條件變量允許線程由於一些暫時沒有達到的條件而阻塞。通常,等待另一個線程完成該線程所需要的條件。條件達到時,另外一個線程發送一個信號,喚醒該線程。
條件變量對應的一組操作是pthread_cond_wait和pthread_cond_signal。
條件變量與互斥量一起使用,一般情況是:一個線程鎖住一個互斥量,然后當它不能獲得它期待的結果時,等待一個條件變量;最后另外一個線程向它發送信號,使得它可以繼續執行。
需要注意的是,pthread_cond_wait會暫時解開持有的互斥鎖。
四、讀寫鎖
讀寫鎖相對上面的問題會復雜一些,它被用來解決一個經典的問題:讀者-寫者問題。
讀寫鎖與互斥量類似,不過讀寫鎖允許更高的並行性。互斥量要么是鎖住狀態要么是不加鎖狀態,而且一次只有一個線程可以對其加鎖。
下面的代碼考慮的是讀者優先的讀者-寫者問題,對於共享區域的讀寫規則如下:
1.只要有一個讀者在讀,后來的讀者可以進入共享區直接讀。
2.只要有一個讀者在讀,寫者就必須阻塞,直到最后一個讀者離開。
3.不考慮搶占式,寫者在寫時,即使有讀者到達,也會在就緒態等待。
typedef int semaphore; semaphore mutex=1; //互斥鎖,控制對rc的訪問 semaphore db=1; //控制對數據庫的訪問 int rc=0; //當前讀者計數 void reader(void) { while(TRUE) { down(&mutex);//加鎖 rc=rc+1; if(rc==1) down(&db);//第一個讀者,加鎖 up(&mutex); read_data_base(); down(&mutex); rc=rc-1; if(rc==0) up(&db);//最后一個讀者離開,解鎖 up(&mutex); use_data_read(); } } void writer(void) { while(TRUE) { think_up_data(); down(&db);//獲取數據庫訪問的鎖 write_data_base(); up(&db); } }
這里,我們其實用了兩個互斥鎖來實現了讀寫鎖。一個互斥鎖用來保護共享區,另外一個互斥鎖用來保護讀者計數器。
讀寫鎖可以由三種狀態:讀模式下加鎖狀態、寫模式下加鎖狀態、不加鎖狀態。一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時占有讀模式的讀寫鎖。
在讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。當讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是如果線程希望以寫模式對此鎖進行加鎖,它必須阻塞直到所有的線程釋放讀鎖。雖然讀寫鎖的實現各不相同,但當讀寫鎖處於讀模式鎖住狀態時,如果有另外的線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨后的讀模式鎖請求。這樣可以避免讀模式鎖長期占用,而等待的寫模式鎖請求一直得不到滿足。
讀寫鎖非常適合於對數據結構讀的次數遠大於寫的情況。當讀寫鎖在寫模式下時,它所保護的數據結構就可以被安全地修改,因為當前只有一個線程可以在寫模式下擁有這個鎖。當讀寫鎖在讀狀態下時,只要線程獲取了讀模式下的讀寫鎖,該鎖所保護的數據結構可以被多個獲得讀模式鎖的線程讀取。
讀寫鎖也叫做共享-獨占鎖,當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當他以寫模式鎖住時,它是以獨占模式鎖住的。
五、總結
這里,主要是簡單總結一下這幾種同步量的用法。
1、互斥鎖只用在同一個線程中,用來給一個需要對臨界區進行讀寫的操作加鎖。
2、信號量與互斥量不同的地方在於,信號量一般用在多個進程或者線程中,分別執行P/V操作。
3、條件變量一般和互斥鎖同時使用,或者用在管程中。
4、互斥鎖,條件變量都只用於同一個進程的各線程間,而信號量(有名信號量)可用於不同進程間的同步。當信號量用於進程間同步時,要求信號量建立在共享內存區。
5、互斥鎖是為上鎖而優化的;條件變量是為等待而優化的; 信號量既可用於上鎖,也可用於等待,因此會有更多的開銷和更高的復雜性。
參考書籍:《現代操作系統》