同步回顧
進程同步控制有多種方式:算法、硬件、信號量、管程
這些方式能夠認爲就是同步的工具(方法、函數)
好比信號量機制中的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),可是確定有對應物。