程序間通訊同步方法(互斥)
程序間通訊(Inter Process Communication, IPC)要解決三個問題:
(1)程序間如何傳遞資訊
(2)確保兩個或更多程序在關鍵活動中不會出現交叉
(3)有協作關係的程序的時序問題
競爭條件(race condition)
定義:多個程序讀寫某些共享資料,而最後的結果取決於程序執行的精確時序。
互斥(mutual exclusion)
定義:對於個共享資料,同一時刻只有一個程序操作他。
臨界區(critical region)
定義:對共享記憶體進行訪問的程式片段稱作臨界區
避免競爭條件的一個好的解決方案還應該保證併發程序能夠正確和高效地進行協作,因此要滿足一下條件:
(1)任何兩個程序不能同時處於其臨界區
(2)臨界區外執行的程序不得阻塞其他程序
(3)不得使程序無限期等待進入臨界區
(4)不應對CPU的速度和數量做任何假設
互斥方案
當一個程序進入臨界區其他個程序將不可以進入,這樣就不會帶來麻煩;//ps同步程式碼塊?
(1)遮蔽程式碼塊
CPU在發生時鐘中斷時才會切換執行緒,程序進入臨界區後遮蔽掉所有中斷,並在離開臨界區時開啟中斷,這樣就可以實現同一時刻只有一個程序讀寫共享資料。
缺點:(1)遮蔽中斷地權力交給使用者時不明智地(2)遮蔽中斷僅對執行disable指令的CPU有效;
遮蔽終端對作業系統本身而言是很有用的(遮蔽中斷以重新整理資料),但對使用者程序而且是一種不合適額互斥機制。
(2)鎖變數
共享(鎖)變數 鎖為0 表示沒有程序進入臨界區,否則有程序進入臨界區
鎖自身也是共享資料,也存在競爭,鎖是不安全的!
(3)嚴格輪換法(自旋鎖)
忙等待:while(true){...};
嚴格輪轉法採用忙等待,即連續測試一個變數直到某個值出現為止,用於忙等待的鎖稱為自旋鎖(spin lock),這種方式比較浪
費CPU時間,通常應該避免。此方法會阻塞其他程序所以不是一個好的方法。
(4)peterson解法
int turn; //鎖變數
int interested[N];
void enter_region(int process){
int other;
other = 1 - process; //其他程序
interested[process] = True;
turn = process;
while(turn==process && interested[other]==True); //等待other離開臨界區
}
void leave_region(int process){
}
(5)TSL(Test and Set Lock)
需要硬體支援的一種方案:將一個記憶體字lock獨到暫存器中,然後在該記憶體地址上存一個非0值,讀字和寫字操作保證是不可分割的,即指令結束之前其他處理器均不允許訪問該記憶體字。(是不是類似CAS【compare and swap】?),執行TSL指令的CPU將鎖住記憶體匯流排,以禁止其他CPU在本指令結束之前訪問記憶體。
(6)睡眠與喚醒( sleep and wakeup【生產者和消費者問題/有界緩衝區】)
睡眠是將一個無法進入臨界區的程序阻塞,而不是忙等待,該程序被掛起,直到另外一個程序將其喚醒。
#define N 100 //緩衝區大小
int count = 0;
//資料生產者
void producer(void){
int item;
while(True){
item = produce_item(); //產生下一新資料項
if(count==N) sleep(); //如果緩衝區滿,就進入休眠狀態
insert_item(item); //將新資料項放入緩衝區
count++;
if(count==1) wakeup(consumer);//喚醒消費者
}
}
//資料消費者
void consumer(void){
int item;
while(True){
if(count==0)sleep(); //緩衝區為空,就進入休眠狀態
item = remove_item(); //從緩衝區取走一個數據項
count--;
if(count==N-1)wakeup(producer);//喚醒生產者
consume_item(item);
}
}
在生產者-消費者(producer-consumer)問題中,兩個程序共享一個公共的固定大小的緩衝區(bounded-buffer),其中一個是生產者,將資訊放入緩衝區;另一個是消費者,從緩衝區取走資訊。當緩衝區滿時,讓生產者睡眠,待消費者從緩衝區取出一個或多個數據項時再喚醒它。同樣地,當消費者試圖從空緩衝區取資料時,消費者就睡眠,直到生產者向其中放入一些資料項時再喚醒它。
存在的問題:緩衝區的計數器count 並不是安全的,該共享資源會出先競爭條件。當判斷count滿足睡眠條件時,但還未sleep,此時收到的wakeup訊號將不起作用,倒是wakeup訊號丟失;
解決方法:啟用喚醒等待為,當放鬆一個wakeup訊號之後將wakeup置1,在睡眠之前判斷安wakeup位是否為1 ,如果是則不sleep 並清楚wakeup位。
訊號量Semophore
原子操作:一組相關聯的操作要麼都不間斷的執行,要麼都不執行。
訊號量是設定一個整型變數來累計喚醒次數。對訊號量有兩種操作:down和up(一般化後的sleep和wankeup)。
down操作,則是先檢查其值是否大於0,若該值大於0,則將其值減1並繼續,若該值為0,則程序將睡眠。這裡,檢查數值、修
改變數值以及可能發生的睡眠操作是一個原子操作。
up操作對訊號量加1,訊號量的增值1和喚醒操作同樣是不可分割的。
二元訊號量:保證同時只有一個訊號量進入臨界區 其值為0 、1;
訊號量優化生產者消費者問題
#define N 100 //緩衝區大小
typedef int semaphore;
semaphore full = 0; //緩衝區已用數目
semaphore empty = N; //緩衝區剩餘數目
semaphore mutex = 1; //控制對臨界區的訪問
//資料生產者
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);
}
}
互斥量
沒有訊號量的計數能力,訊號量的一個簡化版本,互斥量是一個可以處於兩態之一的變數:加鎖和解鎖
如果互斥量是解鎖的,則執行緒可以自由進入臨界區,如果互斥量是加鎖的,呼叫執行緒被阻塞,直到臨界區中的執行緒完成並呼叫mutex_unlock,如果多個執行緒被阻塞在互斥量上將隨機選擇一個執行緒允許它獲得鎖。
互斥鎖的實現依靠TSL或者XCHG指令(JAVA CAS?)
條件變數
在C語言中
{
pthread_mutex_lock(&mutex);
while(...) pthread_con_wait(&condp,&mutex);
......
pthread_mutex_unlock(&mutex);
}
{
pthread_mutex_lock(&mutex);
pthread_con_siganl(&condc);
......
pthread_mutex_unlock(&mutex);
}
條件變數允許執行緒由於一些未達到的條件而阻塞;
條件變數和互斥量經常一起使用,用於讓一個執行緒鎖住一個互斥量,然後當他不能獲得他期待的結果是等待一個條件變數,最後另一個執行緒會向他傳送訊號,然後繼續判斷條件;
條件變數不是計數器,條件變數也不能像訊號量那樣累加以便以後使用,所以如果向一個條件變數傳送訊號,但此時條件變數並沒有在條件上阻塞(等待程序),則該訊號無效(丟失)。換而言之,wait必須在signal之前。
管程(monitor)
一個管程是一個由過程、變數及資料結構等組成的一個集合,它們組成一個特殊的模組和軟體包。程序可在仍需需要的時候呼叫管程的過程,但不能在管程之外申明的過程中訪問管程的資料結構。
管程中任一時刻管程中只能有一個程序活躍。java可以實現管程
通過將訊號量放在共享記憶體中並使用TSL或者XCHG指令來保護他們可以壁面競爭,但是在一個分散式系統中(區域網相連)這些原語將失效。
訊息傳遞
send
receive
如果沒有訊息可用,接收者可能被阻塞,直到一條訊息到達,或者帶著一個錯誤碼立即返回。
屏障
用於程序組之間同步的。
將應用劃分為若干階段,只有當所有執行緒都準備就緒著手下個階段,否則任何程序都不能進入下個階段,通過在階段結尾設定屏障來攔截先到的執行緒。
java中的java.util.concurrent.CyclicBarrier (柵欄) 就是屏障的一個實現