1. 程式人生 > >進程同步經典示例 多線程上篇(五)

進程同步經典示例 多線程上篇(五)

put 主程序 進程間互斥 結束 只需要 pla 並發執行 是什麽 消費者

同步回顧 進程同步控制有多種方式:算法、硬件、信號量、管程 這些方式可以認為就是同步的工具(方法、函數) 比如信號量機制中的wait(S) 和 signal(S) ,就相當於是兩個方法調用。 調用wait(S)就會申請這個資源,否則就會等待(進入等待隊列);調用signal(S)就會釋放資源(或一並喚醒等待隊列中的某個); 在梳理同步問題的解決思路時,只需要合理安排方法調用即可,底層的實現細節不需要關註。 接下來以這種套路,看一下借助與不同的同步方式“算法、硬件、信號量、管程”這一“API”,如何解決經典的進程同步問題 技術分享圖片

生產者消費者


生產者-消費者(producer-consumer)問題是一個著名的進程同步問題。它描述的是: 有一群生產者進程在生產產品,並將這些產品提供給消費者進程去消費。 為使生產者進程與消費者進程能並發執行,在兩者之間設置了一個具有 n 個緩沖區的緩沖池,生產者進程將它所生產的產品放入一個緩沖區中;消費者進程可從一個緩沖區中取走產品去消費。 盡管所有的生產者進程和消費者進程都是以異步方式運行的,但它們之間必須保持同步 也就是即不允許消費者進程到一個空緩沖區去取產品,也不允許生產者進程向一個已裝滿產品且尚未被取走的緩沖區中投放產品。

記錄型信號量

對於緩沖池本身,可以借助一個互斥信號量mutex實現各個進程對緩沖池的互斥使用; 生產者關註於緩沖池空位子的個數,消費者關註的是緩沖池中被放置好產品的滿的個數 技術分享圖片 所以,我們總共設置三個信號量semaphore mutex值為1,用於進程間互斥訪問緩沖池 full表示緩沖區這一排坑中被放置產品的個數,初始時為0 empty表示緩沖區中空位子的個數,初始時為n 對於緩沖池以一個數組的形式進行描述:buffer[n] 另外還需要定義兩個用於對數組進行訪問的下標 in out ,初始時都是0,也就是生產者會往0號位置放置元素,消費者會從0號開始取 每次的操作之後,下標後移,in和out采用自增的方式,所以應該是循環設置,比如in為10時,應該從頭再來,所以求余(簡言之in out序號一直自增,通過求余循環)
//變量定義
int in=0, out=0;
item buffer[n];
semaphore mutex=l,empty=n, full=0;
 
//生產者
void proceducer(){
do{
producer an item nextp;
......
wait(empty);//等待空位子
wait(mutex);//等待緩沖池可用
buffer[in] =nextp;//設置元素
in =(in+1)%n;//下標後移
signal(mutex);//釋放緩沖池
signal(full);//“滿”也就是已生產產品個數釋放1個(+1)
}while(TRUE);
 
 
//消費者
void consumer() {
do{
wait(full);//等待已生產資源個數
wait(mutex);//等待緩沖池可用
nextc= buffer[out];//獲得一個元素
out =(out+1) % n;//下標後移
signal(mutex);//釋放緩沖池
signal(empty);//空位子多出來一個
consumer the item in nextc;//消費掉獲得的產品
} while(TRUE);
}
 
//主程序
void main() {
proceducer();
consumer();
}
以上就是一個記錄型信號量解決生產者消費者的問題的思路 對於信號量中用於實現互斥的wait和signal必須是成對出現的,盡管他們可能位於不同的程序中,這都無所謂,他們使用信號量作為紐帶進行聯系 技術分享圖片

AND型信號量

對於生產者和消費者,都涉及兩種資源,一個是緩沖池,一個是緩沖池空或滿 所以可以將上面兩種資源申請的步驟轉換為AND型,比如 wait(empty);//等待空位子 wait(mutex);//等待緩沖池可用 轉換為AND的形式的Swait(empty,mutex)
int in=0, out=0;
item buffer[n];
semaphore mutex=l, empty=n, full=O;
void proceducer() {
do{
producer an item nextp;
......
Swait(empty, mutex);
buffer[in] = nextp;
in =(in+1) % n;
Ssignal(mutex, full)
} while(TRUE);
}
void consumer() {
do{
Swait(full, mutex);
nextc= buffer[out];
out =(out+1) % n;
Ssignal(mutex, empty);
consumer the item in nextc;
......
} while(TRUE);
}
這個示例中,AND型信號量方案只是記錄型信號量機制的一個簡單升級

管程方案

管程由一組共享數據結構以及過程,還有條件變量組成。 共享的數據結構就是緩沖池,大小為n 生產者向緩沖池中放入產品,定義過程put(item) 消費者從緩沖池中取出產品,定義過程get(item) 對於生產者,非滿 not full 就可以繼續生產數據; 對於消費者,非空 not empty 就可以繼續消費數據; 所以設置兩個條件:notfull,notempty 如果數據個數 count>=N,那麽 notfull 非滿條件不成立 如果數據個數 count<=0,那麽notempty 非空條件不成立 也就是說: count>=N,notfull 不滿足,生產者就會在 notfull 條件上等待 count<=0N,notempty 不滿足,消費者就會在 notempty 條件上等待
//定義一個管程
Monitor procducerconsumer {
item buffer[N];//緩沖區大小
int in, out;//訪問下標
condition notfull, notempty;//條件變量
int count;//已生產產品的個數
//生產方法
void put(item x) {
if(count>=N){
notfull.wait; //如果生產個數已經大於緩沖區大小,將生產進程添加到notfull條件的等待隊列中
}
buffer[in] = x; //設置元素
in = (in+1) % N; //下標移動
count++;//已生產產品個數+1
notempty.signal //釋放等待notempty條件的進程
}

//獲取方法
void get(item x) {
if(count<=0){
notempty.wait; // 如果已生產產品數量為0(以下),消費者進程添加到notempty的等待隊列中
}
x = buffer[out];// 讀取元素
out = (out+1) % N; // 下標移動
count--; //已生產產品個數-1
notfull.signal; // 釋放等待notfull條件的進程
}
//初始化數據方法
void init(){
in=0;out=0;count=0;
}
} PC;
生產者和消費者邏輯
void producer(){
item x;
while(TRUE){
produce an item in nextp;
PC.put(x);
}
}
void consumer( {
item x;
while(TRUE) {
PC.get(x);
consume the item in nextc;
......
}
}
void main(){
proceducer();
consumer();
}
管程的解決思路就是將同步的問題封裝在管程內部,管程會幫你解決所有的問題

哲學家進餐


由Dijkstra提出並解決的哲學家進餐問題(The Dinning Philosophers Problem)是典型的同步問題。 該問題是描述有五個哲學家共用一張圓桌,分別坐在周圍的五張椅子上,在圓桌上有五個碗和五只筷子,他們的生活方式是交替地進行思考和進餐。 平時,一個哲學家進行思考,饑餓時便試圖取用其左右最靠近他的筷子,只有在他拿到兩只筷子時才能進餐。 進餐完畢,放下筷子繼續思考。 技術分享圖片 灰色大圓桌,黃色凳子,每個人左右各有一根筷子,小圓點表示碗。(盡管畫的像烏龜,但這真的是桌子  ̄□ ̄||)

記錄型信號量機制

放在桌子上的筷子是臨界資源,同一根筷子不可能被兩個人同時使用,所以每一根筷子都是一個共享資源 需要使用五個信號量表示,五個信號量每個表示一根筷子 當哲學家饑餓時,總是先去拿他左邊的筷子,即執行wait(chopstick[i]); 成功後,再去拿他右邊的筷子,即執行wait(chopstick[(i+1)mod 5]);又成功後便可進餐。(i+1)mod 5 是為了處理第五個人右邊的是第一個的問題 ) 進餐完畢,又先放下他左邊的筷子,然後再放右邊的筷子。
//定義五個信號量
//為簡單起見,假定數組起始下標為1
//信號量全部初始化為1
semaphore chopstick[5]={1,1,1,1,1};
do{
//按照我們上面圖中所示,第 i號哲學家,左手邊為i號筷子,右手邊是 (i+1)%5
wait(chopstick[i]);//等待左手邊的,
wait(chopstick[(i+1)%5]);]);//等待右手邊的
// 進餐......
signal(chopstick[i]);//釋放左手邊的
signal(chopstick[(i+1)%5])//釋放右手邊的
// 思考......
} while(TRUE);
通過這種算法可以保證相鄰的兩個哲學家之間不會出現問題,但是一旦五個人同時拿起左邊的筷子,都等待右邊的筷子,將會出現死鎖 有幾種解決思路 (1)至多只允許有四位哲學家同時去拿左邊的筷子 可以保證肯定會空余一根筷子,並且沒拿起筷子的這個人的左手邊的這一根,肯定是已經拿起左手邊筷子的某一個人的右手邊,所以肯定不會死鎖 (2) 僅當哲學家的左、右兩只筷子均可用時,才允許他拿起筷子進餐。 也就是AND機制,將左右操作轉化為“原子” (3) 規定奇數號哲學家先拿他左邊的筷子,然後再去拿右邊的筷子,而偶數號哲學家則相反。 如上圖所示,1搶1號筷子,2號和3號哲學家競爭3號筷子,4號和5號哲學家競爭5號筷子,所有人都是先競爭奇數,然後再去競爭偶數 這一條是為了所有的人都會先競爭奇數號筷子,那麽也就是最多三個人搶到了奇數號筷子,有兩個人第一步奇數號筷子都沒搶到的這一輪就相當於出局了 三個人,還有兩個偶數號筷子,必然會有一個人搶得到

AND型信號量

哲學家進餐需要左手和右手的筷子,所以可以將左右手筷子的獲取操作原子化,借助於AND型信號量
//定義五個信號量
//為簡單起見,假定數組起始下標為1
//信號量全部初始化為1
semaphore chopstick[5]={1,1,1,1,1};

do{
//按照我們上面圖中所示,第 i號哲學家,左手邊為i號筷子,右手邊是 (i+1)%5
Swait(chopstick[i],chopstick[(i+1)%5]))
// 進餐......
Ssignal(chopstick[i],chopstick[(i+1)%5]);
// 思考......
} while(TRUE);

讀者寫者問題


一個數據文件或記錄,可被多個進程共享,我們把只要求讀該文件的進程稱為“Reader進程” ,其他進程則稱為“Writer 進程” 。 允許多個進程同時讀一個共享對象,因為讀操作不會使數據文件混亂。 但不允許一個Writer 進程和其他Reader 進程或 Writer 進程同時訪問共享對象,因為這種訪問將會引起混亂。 所謂“讀者—寫者問題(Reader-Writer Problem)”是指保證一個 Writer 進程必須與其他進程互斥地訪問共享對象的同步問題。 讀者—寫者問題常被用來測試新同步原語。
很顯然,只有多個讀者時不沖突 技術分享圖片

記錄型信號量機制

讀和寫之間是互斥的,所以需要一個信號量用於讀寫互斥Wmutex 另外如果有讀的進程存在,另外的進程如果想要讀的話,不需要同步也就是Wait(Wmutex)操作; 如果當前沒有進程在讀,那麽需要Wait(Wmutex)操作,所以設置一個變量記錄寫者個數Readcount,可以用來判斷是否需要同步 另外Readcount 會被多個讀者進程訪問,所以也是臨界資源,所以設置一個rmutex 用於互斥訪問Readcount
//兩個信號量,一個用於讀者互斥 readcount ,一個用於讀寫互斥
semaphore rmutex=l,wmutex=1;
int readcount=0;//初始時讀者個數為0
//讀者
void reader() {
do{
wait(rmutex);//讀者先獲取 readcount
if(readcount==0){//如果一個讀者沒有,第一個讀者需要與寫者互斥訪問
wait(wmutex);
}
readcount++;//讀者個數+1
signal(rmutex);//讀者個數+1後,可以釋放readcount的鎖,其他讀者可以進來
//開始慢慢讀書......
wait(rmutex);//讀者結束時,需要獲取readcount的鎖
readcount--;//退出一個讀者
if (readcount==0) {//如果此時一個讀者都沒有了,還需要釋放與讀寫互斥的鎖
signal(wmutex);
}
signal(rmutex);//釋放readcount的鎖
}while(TRUE);
}

void writer(){
do{
wait(wmutex);//寫者必須獲得wmutex
//執行寫任務....
signal(wmutex);//寫任務結束後就可以釋放鎖
}while(TRUE);

}

//主程序
void main() {
reader();
writer();
}
寫者相對比較簡單,獲得鎖wmutex之後,進行寫操作,否則等待wmutex 讀者也是需要先獲得鎖,讀操作後釋放鎖,但是因為多個讀者之間互不影響,所以使用readcount記錄讀者個數,只有第一個讀者才需要競爭wmutex,只有最後一個讀者才需要釋放wmutex readcount作為讀者之間的競爭資源,所以對readcount進行操作的時候也需要進行加鎖

信號量集機制

將讀者寫者的問題復雜化一點,它增加了一個限制,即最多只允許 N個讀者同時讀。 在上面的解決方法中,可以不使用rmutex控制對readcount的互斥,可以構造一個讀者個數的信號量readcountmutex,初始值設置為N 每次新增一個讀者時,wait(readcountmutex),一個讀者離開時signal(readcountmutex) 也可以使用信號量集機制
int N;//最大的讀者個數,也就是相當於圖書館的空位子,初始時空位子為N

semaphore L=N, mx=1;//定義兩個信號量資源L和mx,分別用於控制讀者個數限制和讀寫(寫寫)

void reader() {

do{

Swait(L, 1, 1);//獲取空位子L,每次獲取1個,>=1時可分配

Swait(mx, 1, 0);//獲取與寫的互斥量mx,每次獲取0個,>=1時可分配,如果mx為1,也就是沒有寫者,讀者都可以進來,否則一個都進不來

//進行一些讀操作

Ssignal(L, 1);//釋放一個單位的資源L

}while(TRUE);

}
 

void writer() {

do{

Swait(mx,1,1; L,N,0);//獲得資源mx,每次獲取1個,>=1時分配,獲得資源L,每次獲得0個,>=N時即可分配

//進行一些寫操作

Ssignal(mx, 1);//釋放資源mx

}while(TRUE);

}


void main(){

reader();

writer();

}
Swait(L, 1, 1);用於獲取讀者空位子沒什麽好說的 Swait(mx, 1, 0);作為開關,只要mx滿足條件>=1,那麽就可以無限制的進入(此例中有L的限制),一旦條件不滿足,則全都不能進入,滿足多讀者,有寫不能讀的情況 對於寫者中的Swait(mx,1,1; L,N,0); 他會獲取mx,>=1時,獲取一個資源,並且當L>=N時,分配0個L資源,也就是說一個讀者都沒有的時候才行 Swait(mx, 1, 0); 與Swait( L,N,0);都是需求0個,相當於開關判斷

總結

以上為借助“進程同步的API”,信號量,管程等方式完成進程同步的經典示例,例子來源於《計算機操作系統》 說白了,就是用 wait(S) Swait(S) signal(S) Ssignal(S)等這些“方法”描述進程同步算法 可能會覺得這些內容亂七八糟的,根本沒辦法使用,的確這些內容全都沒辦法直接轉變為代碼寫到你的項目中 但是,這些都是解決問題的思路 不管是信號量還是管程還是什麽,不會需要你從頭開始實現一個信號量,然後.......也不需要你從頭開始實現一個管程,然後...... 不管是操作系統層面,還是編程語言層面,還是具體的API,萬變不離其宗 盡管這些wait和signal的確不存在,但是,但是,但是編程語言中很可能已經提供了語意相同的方法供你調用了 也就是說,你只需要理解同步的思路即可,盡管沒有我們此處說的wait(S),但是肯定有對應物。 原文地址:進程同步經典示例 多線程上篇(五)

進程同步經典示例 多線程上篇(五)