0x10 進程同步

多道程序設計技術是現代操作系統的基礎。在進程併發執行時。各個協同進程運行次序的不同會導致不同的運行結果,從而出現運行錯誤。

數據不一致性

多道程序設計技術和多核處理器在觀代操作系統中廣泛應用,系統中的多個進程併發或並行執行已經成爲常態。每個進程可在任何時候被中斷,僅僅進程的部分代碼片段可連續執行。

共享數據併發/並行訪問:數據不一致性,又稱不可再現性:同一進程在同一批數據上多次運行的結果不一樣。

數據不一致性例子:有界緩衝

例子:n個緩衝區的有界緩衝問題

  • 增加變量counter,初始化爲0;
  • 向緩衝區增加一項時,counter加1;
  • 從緩衝區移去一項時,counter減1。
    在這裏插入圖片描述
  • 數據結構:
Shared data
#define BUFFER SIZE 10
typedef struct{
	...
}item; 
item buffer[BUFFER_SIZE]; 
int in=0;int out=0; 
int counter=0;

生產者進程enter()

item nextProduced;

while(1){
while(counter==BUFFER_SIZE)
;/*do nothing*/
buffer[in]=nextProduced;
in=(in +1)%BUFFER_SIZE;
counter++;
}

消費者進程remove()
item nextConsumed;
while(1){
while(counter==0)
;/*do nothing*/
nextConsumed=buffer[out];
out=(out +1)%BUFFER_SIZE;
counter- -;
}

“counter++”的僞機器語言:
(SO)register1=counter
(S1)register1=register1+1
(S2)counter=register1

“counter–”的僞機器語言:
(S3)register2=counter
(S4)register2=register2-1
(S5)counter=register2

例題:
在有界緩衝問題中,生產者中的語句counter++和消費者中的語句counter—對應的僞機器指令如下:
“counter++”的僞機器語言:
(S0)register1 = counter
(S1)register1 = register1 + 1
(S2)counter = register1
“counter—”的僞機器語言:
(S3)register2 = counter
(S4)register2 = register2 – 1
(S5)counter = register2
假如當前counter=2,則可能存在()種不同的執行結果。

A. 1
B.2
C.3
D.以上都不對

C

運行錯誤例子:
初時counter=5:
S0:register1=counter {register1=5}
S1:register1=register1+1 {register1=6}
S3:register2=counter {register2=5}
S4:register2=register2-1 {register2=4}
S2:counter=register1 {counter=6}
S5:counter=register2 {counter=4}
在這裏插入圖片描述
解決方法
規定這6個語句的運行次序,把counter++和counter–的語句分別作爲一個整體來運行,也就是counter++或counter–的三個語句必須分別連續運行,不可中斷。
例如:
在這裏插入圖片描述
原子操作:一個操作在整個執行期間不能被中斷。

導致數據不一致性的原因:

競爭條件:多個進程併發訪問同一共享數據,而共享數據的最終結果取決於最後操作的進程。

防止競爭條件方法:併發進程同步或互斥。

同步和互斥

同步:協調進程的執行次序,使併發進程間能有效地共享資源和相互合作,保證數據一致性。
協調執行次序。

互斥:進程排他性地運行某段代碼,任何時候只有一個進程能夠運行

臨界資源

  • 臨界資源是指一次只允許一個進程使用的資源,又稱互斥資源、獨佔資源或共享變量

  • 共享資源:一次允許多個進程使用的資源。

  • 臨界資源例子:
    許多物理設備都屬於臨界資源,如輸入機、打印機、磁帶機等,還有上例中的counter屬於臨界資源。

臨界區

臨界區是指涉及臨界資源的代碼段

臨界區是代碼片段;
是進程內的代碼;
每一個進程有一個或多個臨界區;
臨界區的設置方法由程序員確定。

若能保證諸進程互斥進入關聯的臨界區,可實現對臨界資源的互斥訪問。

臨界區例子:
在這裏插入圖片描述

臨界區使用準則

如何實現進程之間互斥使用臨界區,從而保證數據的一致性呢?

  1. 互斥
    假定進程Pi在某個臨界區執行,其他進程將被排成在該臨界區外;
    有相同臨界資源的臨界區都需互斥;
    無相同臨界資源的臨界區不需互斥。
  2. 有空讓進
    臨界區內無進程執行,不能無限期地延長下一個要進臨界區進程的等待時間。
    意思就是每個離開臨界區的進程應該開放臨界區,讓等待進入的進程可以進入。
  3. 有限等待
    每個進程進入臨界區前的等待時間必須有限,不能無限等待。
    意思就是每個進程的臨界區大小必須儘可能小,否則其他進程可能長時間等待。

訪問臨界區過程:
在這裏插入圖片描述

  • 進入區實現互斥準則,保證任何進程可以進入臨界區;
  • 退出區實現有空讓進準則
  • 每個臨界區不能過大,從而實現有限等待準則。

信號量

信號量就是同步機制的一種實現方法。
早期硬件解決辦法,程序設計人員認爲太複雜。

後來提出了軟件解決方法:

  • 保證兩個或多個代碼段不被併發調用;
  • 在進入關鍵代碼段前,進程必須獲取一個信號量,否則不能運行;
  • 執行完該關鍵代碼段,必須釋放信號量;
  • 信號量有值,爲正說明它空閒,爲負說明其忙碌。

整型信號量:

信號量S——整型變量

提供兩個不可分割的[原子操作]訪問信號量。

wait(S):
	while S<=0 do no-op;
	S--;
signal(S):
	S++;

wait(S)又稱爲P(S),signal(S)又稱爲V(S)

整型信號量的問題是忙等。如果S<=0,該進程將不斷重複執行while語句,在耗費CPU資源的同時,進程沒有往前推進,造成了CPU的浪費。
忙等的解決方法是引入記錄型信號量。記錄型信號量增加了一個等待隊列。當一個進程無法獲得一個信號量時。馬上釋放CPU並把自己轉換爲等待狀態。加入該信號量的等待隊列。從而消除忙等。

記錄型信號量定義:

typedef struct{
	int value;
	struct process *list;
}semaphore;

記錄型信號量的wait操作:

Wait(semaphore *S){
	S->value--;//將信號量S的值分別減1,申請一個S信號量
	if(S->value<0){//該進程無法獲得一個S信號量,加入等待隊列S->list
		add this process to list S->list;
		block();
	}
}

記錄型信號量是先把信號量的值減1再判斷。而整型信號量是先判斷再減1。
目的是可以知道由於申請該信號量而被阻塞的進程數量。
當S是一個負數時,|S|表示S的等待隊列中等待該信號量的進程數目。

記錄型信號量的signal操作:

Signal(semaphore *S){
	S->value++;//釋放S信號量
	if(S->value<=0){//有進程在等待S信號量
		remove a process P from list S->list;//從隊列首部遺棄一個進程P
		wakeup(P);//喚醒等待的進程
	}
}

記錄型信號量的改進在於加入了阻塞和喚醒機制,消除了忙等

信號量類型

  1. 計數信號量
    變化範圍:沒有限制的整型值;
    計數信號量=同步信號量

  2. 二值信號量
    變化範圍僅限於0和1的信號量(整型信號量);
    二值信號量=互斥信號量

信號量的使用:

S必須置一次且只能置一次初值;
S初值不能爲負數;
除了初始化,只能通過執行P、V操作來訪問S,不能在代碼中直接訪問信號量S

互斥信號量使用(在臨界區):

Semaphore *S;//初始化爲1

wait(S);//在臨界區前執行wait操作,申請信號量,申請到進入臨界區,申請不到就阻塞
CriticalSection() //臨界區
signal(S);

同步信號量使用

同步信號量比較複雜。一般情況下,同步信號量的wait和signal操作位於兩個不同進程內。

例子:P1和P2需要C1比C2先運行
在這裏插入圖片描述

semaphore s=0
P1:
     C1;
     signal(s); //把s的值設爲1
P2:
     wait(s); //申請到信號量s
     C2;

使用同步和互斥機制來保證生產者和消費者在併發執行時的數據一致性。

三類經典同步問題:

  1. 生產者-消費者問題
    共享有限緩衝區
  2. 讀者寫者問題
    數據讀寫操作
  3. 哲學家就餐問題
    資源競爭

生產者消費者問題

問題描述

在這裏插入圖片描述
生產者消費者問題的本質是如何實現生產者和消費者之間的同步和互斥。

生產者:{
	...
	生產一個產品
	...
	把產品放入指定緩衝區
}

消費者:{
	...
	從指定緩衝區取出產品
	...
	消費取出的產品
}

互斥分析基本方法:

①分析每個進程的代碼,查找臨界資源。
②劃分臨界區;
③定義互斥信號量並賦初值,一般互斥信號量的初值爲1;
④在臨界區前的進入區加wait操作,在臨界區後的退出區加signal操作。

生產者消費者的互斥分析

生產者(多個):

buffer[in]=nextProduced;
in = (in+1) % BUFFER_SIZE;
counter++;

把產品放入指定緩衝區;
in:所有的生產者對in指針需要互斥;
counter:所有生產者消費者進程對counter互斥,否則會導致數據不一致性。

消費者(多個):

nextConsumed = buffer[out];
out = (out+1) % BUFFER_SIZE;
counter--;

從指定緩衝區取出產品;
out:所有的消費者對out指針需要互斥;
counter:所有生產者消費者進程對counter互斥。

根據以上的互斥分析可知,多個生產者由於共享變量in需要互斥訪問生產者的臨界區;多個消費者由於共享變量out需要互斥訪問消費者的臨界區;生產者和消費者由於共享變量counter需要互斥訪問生產者和消費者的臨界區。所以任何時候只有一個進程可以進入以上談到的兩個臨界區中的一個。

增加互斥機制:
在這裏插入圖片描述

同步分析基本方法

①找出需要同步的代碼片段(關鍵代碼);
②分析這些代碼片段的執行次序,誰先誰後,有沒有一定的次序;
③根據次序分析。增加同步信號量並賦初始值;
④在關鍵代碼前後分別加wait和signal操作。

同步分析較爲困難。

生產者消費者的同步分析

  1. 分析關鍵代碼
    兩者需要協同的部分:
    生產者:把產品放入指定緩衝區(關鍵代碼C1)
    消費者:從滿緩衝區取出一個產品(關鍵代碼C2)
  2. 分析C1和C2的運行次序
    三種運行次序(不同條件下不同運行次序)
    在這裏插入圖片描述
    所有緩衝區空時,消費者必須等生產者生產出產品後才能消費即C1先於C2運行;
    所有緩衝區滿時,生產者必須要等消費者消費完才能生產產品即C2先於C2運行;
    緩衝區有空也有滿時,既可以先C1後C2,也可以先C2後C1

算法描述

在這裏插入圖片描述
在這裏插入圖片描述

同步信號量定義

共享數據:
semaphore *full,*empty,*m; //full:滿緩衝區數量 empty:空緩衝區數量

初始化:
full->value=0; empty->value=N; //N是緩衝區個數m->value=1;
在這裏插入圖片描述
在這裏插入圖片描述

讀者寫者問題

兩組併發進程:讀者和寫者共享一組數據區進行讀寫;
要求:允許多個讀者同時讀;不允許讀者、寫者同時讀寫;不允許多個寫者同時寫。

第一類讀者寫者問題——讀者優先

讀者:
無讀者、寫者,新讀者可讀;
有寫者等,但有其它讀者在讀,則新讀者也可讀;
有寫者寫,新讀者等。

寫者:
無讀者和寫者,新寫者可寫,阻止其他讀者和寫者進入;
有讀者,新寫者等待;
有其他寫者,新寫者等待。

解決辦法

讓所有的讀者、寫者進程的讀寫都互斥。
臨界區在讀者進程中是讀操作,在寫者進程中是寫操作。
Semephore *W; //互斥信號量W
W->value=1;
在這裏插入圖片描述
這種模式要求讀者之間也要互斥。違背了“有寫者在等,但有其它讀者在讀時,則新讀者可進入數據區讀”這個要求。當一個讀者獲得信號量W進入數據區讀後,後續的讀者無法繼續進入數據區,不能實現讀共享。

解決方法
讀者進入數據區讀時,區分第一個讀者和其它讀者;讀者離開數據區時,區分最後離開的讀者和其它讀者。
當第一個讀者進入數據區之後,應該不讓寫者進入數據區寫,讓其他讀者可以進入讀;
當一個讀者離開數據區時,如果它不是最後一個讀者時,可以直接離開數據區,否則,需要允許後面等待的寫者進入數據區。

修改方法:
增加一個讀者計數器rc,設置初始值爲0;
在這裏插入圖片描述
if(rc==1)
是:執行P(W)。阻止寫者進入數據區寫。

if(rc==0)
不是:直接離開。
是:則需要執行V(W)。查看W的等待隊列中是否有寫者等待。如果有則喚醒一個等待的寫進程。這樣,寫者就可以進入寫了。

由於讀者計數器rc可能被多個讀者進程同時讀,可能會導致數據一致性問題。rc是一個臨界資源。

再增加一個互斥信號量M,設置初始值爲1;
在這裏插入圖片描述

哲學家就餐問題

在這裏插入圖片描述
這是共享資源競爭的例子。把5根筷子分別看做5個互斥信號量。任意一個哲學家只有拿起左右兩根筷子,也就是獲得左右兩個信號量後才能喫飯。喫完飯後,該哲學家應該放下拿起的兩根筷子,也就是釋放左右兩個信號量。
把喫飯看成兩個臨界區,

semaphore *chopstick[5]; //初始值爲1
哲學家i:
	...
	P(chopStick[i]); //拿左邊筷子
	P(chopStick[(i+1)%5]); //拿右邊筷子
	
	喫飯
	
	V(chopStick[i]); //放下左邊筷子
	V(chopStick[(i+1)%5]); //放下右邊筷子
	...

對於任意一個哲學家i來說,在喫飯前要通過P操作,也就是wait操作,拿起自己左右的兩個筷子。P(chopStick[i]) 拿左邊筷子,P(chopStick[(i+1)%5])拿右邊筷子。
V(chopStick[i])放下左邊筷子,V(chopStick[(i+1) % 5])放下右邊筷子。

這種解決方法存在死鎖問題。如果5個哲學家同時拿起了左邊的筷子,這5個哲學家之間對筷子存在循環等待,從而導致他們都無法喫飯,形成了死鎖。如果給5個哲學家6根筷子,則不會有死鎖發生。這種死鎖會導致進程無法推進、資源無法使用,是操作系統必須要解決的。

爲防止死鎖的解決措施

  1. 方法1:最多允許4個哲學家同時坐在桌子周圍;

具體解決方法:

  • 聲明一個同步信號量seat。初始值爲4
  • 規定每個哲學家必須申請到椅子坐下後才能拿筷子
  • 在哲學家喫完飯後,必須從椅子上站起來離開,便於其它哲學家坐下喫飯。
semaphore *chopstick[5]; //初始值爲1
semaphore *seat; //初始值爲4
哲學家i:
	...
	P(seat); //看看4個座位是否有空
	P(chopStick[i]); //拿左邊筷子
	P(chopStick[(i+1)%5]); //拿右邊筷子
	
	喫飯
	
	V(chopStick[i]); //放下左邊筷子
	V(chopStick[(i+1)%5]); //放下右邊筷子
	V(seat); //釋放佔據的位置
	...
  1. 方法2:僅當一個哲學家左右兩邊筷子都可用時,才允許他拿筷子;
  • 兩根筷子都空閒,則該哲學家可以拿起兩根筷子喫飯;
  • 只要有一根筷子在被其它哲學家使用,那麼兩根筷子都無法拿到。

哲學家分爲3個狀態:
int *state={Thinking, hungry, eating};
設置5個信號量,對應5個哲學家
semaphore *ph[5]; //初始值爲0

void test(int i){
//判斷是否餓了,左邊,右邊哲學家是否在喫飯
	if(state[i]==hungry && state[(i+4)%5]!=eating && state[(i+1)%5!-eating){
		state[i]=eating; //設置哲學家狀態爲eating
		V(ph[i]); //ph[i]設置爲1

在這裏插入圖片描述
3. 方法3:給所有哲學家編號,奇數號哲學家必須首先拿左邊筷子,偶數號哲學家則反之。

信號量值S的含義

S>0:有S個資源可用
S=0:無資源可用
S<0:則|S|表示S等待隊列中的進程個數
P(S):申請一個資源
V(S):釋放一個資源

互斥信號量初始值:一般爲1
同步信號量初始值:0-N的整數

信號量的使用

  • P、V操作成對出現
    互斥操作:P、V操作處於同一進程內
    同步操作:P、V操作在不同進程內
  • 兩個一起的P操作的順序至關重要
    同步與互斥P操作一起時,同步P操作要在互斥P操作前
  • 兩個V操作的次序無關緊要

死鎖:信號量使用不當
在這裏插入圖片描述

管程

信號量機制的問題

  • 信號量機制的優點:
    程序效率高,編程靈活;
  • 信號量機制的問題:
    需要程序員實現,編程困難;
    維護困難;
    容易出錯:
    wait/signal位置錯;
    wait/signal不配對。
  • 解決方法:
    管程,由編程語言解決同步互斥問題;

管程定義

  • Hansen的管程定義
    一個管程定義了一個數據結構和能爲併發進程所執行(在該數據結構上)的一組操作,這組操作能同步進程和改變管程中的數據。
  • 結構
    在這裏插入圖片描述
    monitor monitor-name{
    //共享變量定義
    //操作
    public entry p1(…){…}
    public entry p2(…){…}

    //初始化代碼
    Initialization_code(…){…}

管程功能

  1. 互斥
  • 管程中的變量只能被管程中的操作訪問
  • 任何時候只有一個進程在管程中操作
  • 類似臨界區
  • 由編譯器完成
  1. 同步
  • 條件變量
  • 喚醒和阻塞操作
    x.wait():進程阻塞直到另外一個進程調用x.signal()
    x.signal():喚醒另外一個進程
    在這裏插入圖片描述

條件變量問題

  • 管程內可能存在不止1個進程
    如:進程P調用signal操作喚醒進程Q後
  • 存在的可能
    P等待直到Q離開管程(Hoare)
    Q等待直到P離開管程(Lampson&Redl,MESA語言)
    P的singal操作是P在管程內的最後一個語句(Hansen,並行Pascal)

Hoare管程

在這裏插入圖片描述

  • 進程互斥進入管程
    如果有進程在管程內運行,管程外的進程等待;
    入口隊列:等待進入管程的進程隊列。
  • 管程內進程P喚醒Q後
    P等待,Q運行;
    P加入緊急隊列,緊急隊列的優先級高於入口隊列。
  • condition x;
  • x.wait()
    緊急隊列非空:喚醒第一個等待進程;
    緊急隊列空:釋放管程控制權,允許入口隊列進程進入管程;
    執行該操作進程進入x的條件隊列;
  • x.signal()
    x的條件隊列空:空操作,執行該操作進程繼續運行;
    x的條件隊列非空:喚醒該條件隊列的第一個等待進程;
    執行該操作進程進入就緊急隊列。

哲學家就餐Hoare管程解決方案

在這裏插入圖片描述
每個哲學家按照以下的順序輪流調用操作pickup()和putdown()。
dp.pickup(i)
喫飯
dp.putdow(i)

Linux同步機制

  • 使用禁止中斷來實現短的臨界區
  • 自旋鎖(spinlock)
    調用wait操作不會引起調用者阻塞
  • 互斥鎖(Mutex)
  • 條件變量(Condition Variable)
  • 信號量(Semaphore)

Windows同步機制

  • 事件(Event)
    通過通知操作的方式來保持線程的同步
  • 臨界區(Critical Section)
  • 互斥鎖(Mutex)
  • 自旋鎖(Spinlock)
  • 信號量(Semaphore)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章