注:本文主要參考<<現代操作系統>>2.3.6節
在進程喚醒與睡眠一文中,針對多進程的生產者-消費者問題,我們提出了基於信號量的解決方案.該方案避免了進程在等待其要求的下一步執行條件時進入忙等待狀態.我們使用了三個信號量,信號量mutex
用於保證生產者進程與消費者進程不會同時訪問緩衝區.信號量empty
用於保證當緩衝區滿是生產者被阻塞進入休眠狀態,當緩衝區不滿時其能夠被再次喚醒.信號量full
用於保證當緩衝區空時消費者被阻塞進入休眠狀態,當緩衝區不空時其將被生產者進程喚醒.
信號量可以保存當前計數值,down
操作對當前計數值減1,up
操作對當前計數值加1,信號量也正是通過其當前計數值來判斷是否應當阻塞調用進程.如果當前信號量取值爲0,則down
操作將阻塞當前調用進程.
我們能夠在不利用信號量計數能力的條件下解決該問題呢?我們可以使用互斥量與條件變量.
基於互斥量與條件變量的解決方案
互斥量
互斥量主要用於確保對進程對臨界區的互斥訪問.它有兩個狀態,鎖狀態與解鎖狀態.對應的,對互斥量的兩個可能操作爲加鎖操作與解鎖操作.進程在準備進入臨界區時,將嘗試對當前互斥量加鎖,如果當前互斥量爲鎖狀態,則調用進程將被阻塞.如果當前互斥量爲解鎖狀態,則進程將進入臨界區,互斥量將被轉化爲鎖狀態.在進程準備離開緩衝區時,應當將當前互斥量解鎖,並喚醒阻塞在該互斥量上的進程.如果多個進程阻塞在該互斥量上,則隨機選擇一個進程喚醒.
在如何避免多進程(線程)因競爭條件引發的錯誤?一文中,我們提到,鎖變量(即此處的互斥量)實現的重點在於確保對鎖變量的加鎖操作是原子操作,對鎖變量的加鎖操作包括兩步:1.檢查當前鎖變量取值,2.加鎖.因此必須通過適當的方案保證對加鎖操作是原子性的.
條件變量
條件變量則用於在未達到下一步執行條件時阻塞調用進程.當下一條件滿足時,需要通知當前條件變量取消對調用進程的阻塞.在多進程協作程序中,當前進程的執行通常可能需要滿足一些特定條件,而該條件需要通過其他進程的執行來滿足.因此如果當前進程在調用時被條件變量阻塞,則在其他進程滿足該進程執行條件時,應當負責通知條件變量,取消對等待進程的阻塞.
在生產者-消費者問題中,生產者的執行需要保證當前緩衝區有空餘空間,而這一條件需要消費者進程來提供.因此,如果消費者進程的執行提供了緩衝區有空餘空間這一條件,則消費者進程應當通知條件變量,取消對生產者進程的阻塞.同樣的,消費者進程的執行需要保證當前緩衝區不爲空,而這一條件則需要生產者進程來提供.當生產者進程的執行使得當前緩衝區不爲空時,其需要通知條件變量,取消對消費者進程的阻塞.條件變量的阻塞與喚醒通常命名爲wait()
與signal()
.
互斥量與條件變量對比
比較互斥量與條件變量可以發現,互斥量用於確保當前進程進入臨界區後,不會有其他進程在進入.進程在進入臨界區後,應當對互斥量加鎖,而在進程完成操作後,應當移交其佔有權,將當前互斥量解鎖.因此對互斥量的加鎖與解鎖是由同一個進程執行的.多個進程屬於競爭關係,爭奪互斥量.忽略
而在條件變量中,多個進程則屬於協作關係.當前進程在不滿足執行條件時,調用wait
操作阻塞自己,它需要在其他進程提供符合的條件後,由其他進程調用signal
操作幫助自己取消阻塞狀態.
互斥量與條件變量通常是同時使用的.進程首先使用互斥量確保當前對臨界區的互斥訪問,隨後使用條件變量檢查是否滿足下一步執行條件.我們在喚醒與睡眠最後一段中提到,如果我們首先執行down(&mutex)
,再執行down(&empty)
,則可能使得生產者進程與消費者進程均阻塞,引發死鎖.回到當前例子中,如果進程在對互斥量加鎖後,被條件變量阻塞,那是否也會引發死鎖呢?確實會.爲了避免死鎖,條件變量在阻塞當前調用進程前,必須對互斥量解鎖,以使得其他進程可以執行.因此wait
操作通常接受兩個參數,一個是條件變量,一個是與其協作的互斥量.
關於條件變量,還有一點值得特別指出.不像信號量可以駐留在內存中,如果我們協作進程對某一條件變量調用signal()
,然而此時並沒有進程阻塞在該信號量上,則當前進程將被忽略.因此,程序員在編寫程序時,一定要保證wait
的調用在signal()
之前.這個問題類似於我們在喚醒與睡眠中提到的,如果對進程的wakeup()
操作調用發生在sleep()
之前,則喚醒信號將被忽略.
使用pthread線程庫實現多線程生產者與消費者
我們使用pthread多線程庫解決多線程生產者與消費者問題.
#include <stdio.h>
#include <pthread.h>
#define MAX 10000000000 //需要生產的數量了
pthread_mutex_t the_mutex; //互斥量
pthread_cond_t condc, condp; //創建用於消費者與生產者的條件變量了
int buffer = 0; //生產者與消費者使用的緩衝區.
void * producer(void *ptr){
int i;
for(i = 1; i <= MAX; i++){
pthread_mutex_lock(&the_mutex); //互斥使用緩衝區
while(buffer != 0) pthread_cond_wait(&condp, &the_mutex);
//使用大小爲1的緩衝區,當緩衝區滿時,調用wait操作阻塞當前進程,並解鎖互斥量
buffer = i;
pthread_cond_signal(&condc); //喚醒消費者
pthread_mutex_unlock(&the_mutex); //解鎖互斥量
}
pthread_exit(0);
}
void * consumer(void *ptr){
int i;
for(i = 1; i <= MAX; i++){
pthread_mutex_lock(&the_mutex);
while(buffer == 0) pthread_cond_wait(&condc, &the_mutex);
buffer = 0; //從緩衝區中取出
pthread_cond_signal(&condp);
pthread_mutex_unlock(&the_lock);
}
pthread_exit(0);
}
int main(int argc, char ** argv){
pthread_t pro, con; //定義線程變量
pthread_mutex_init(&the_mutex, 0); //初始化互斥量
pthread_cond_init(&condc, 0); //初始化條件變量
pthread_cond_init(&condp, 0);
pthread_create(&con, 0, consumer, 0); //創建線程
pthread_create(&pro, 0, producer, 0);
pthread_join(pro, 0);
pthread_join(con, 0);
pthread_cond_destroy(&condc);
pthread_cond_destroy(&condp);
pthread_mutex_destroy(&the_mutex);
}
注意到,在條件變量的操作位於臨界區當中,因此不會出現條件變量還未執行wait()
操作便執行signal()
.例如,如果當前消費者判斷buffer ==0 ,則將執行wait()
操作阻塞消費者進程,則這一過程中,其不會被生產者影響,因爲它位於臨界區中.