信號量
信號量 : 表示系統中某種資源的數量, 當它的值大於0時, 表示當前可用資源的數量; 當它的值小於0時, 其絕對值表示等待使用該資源的進程個數
P, V操作 : PV操作由P操作原語和V操作原語(不可中斷)組成,針對信號量進行相應的操作. P操作相當於請求資源, V操作相當於釋放資源
信號量的分類
整型信號量
本質就是一個數, 表示資源數量
int S = 1; // 整型信號量, 初始值為1, 表示系統中有一個資源
void P(int S){ // P操作
while(S <= 0); // 資源數不夠則循環等待
S = S - 1; // 分配資源, 資源數-1
}
void V(int S){ // V操作
S = S + 1;
}
整型信號量的問題 : 存在"忙等", 即上述P操作時, 如果資源不夠, 將一直執行while循環語句, 該進程會一直占用CPU, 為解決這個問題, 引入了記錄型信號量
記錄型信號量
除了記錄資源數, 還加入了等待隊列
定義如下:
typedef struct{
int value; // 剩余資源數
struct process *L; // 等待隊列
}semaphore;
對應的P, V操作實現如下:
void P(semaphore S){
S.value--;
if(S.value < 0){
block(S.L); // 使進程從運行態 -> 阻塞態
} // 如果剩余資源不夠, 利用block原語使進程將進程掛起到S的等待隊列(阻塞隊列)中, 避免"忙等"
}
void V(semaphore S){
S.value++;
if(S.value<=0){
wakeup(S.L); // 使進程從阻塞態 -> 就緒態
} // 釋放資源后, 等待隊列還有進程, 那么利用wakeup原語喚醒該進程
}
后文所使用的信號量均為semaphore即記錄型信號量, 一般我們所說的信號量也均為記錄型
利用信號量實現同步與互斥
同步
同步 : 保證"一前一后"執行兩個操作
利用信號量實現同步 :
semaphore S = 0; // 初始化信號量 = 0
P1(){ // P1進程
xx1; // 操作1
xx2; // 操作2
V(S); // 信號量++
}
P2(){
P(S);
xx3; // 操作3
xx4; // 操作4
}
總結就是 : 在"前"操作之后執行V操作, 在"后"操作之前執行P操作
互斥
互斥 : 實現對臨界資源(一次只能供一個進程訪問的資源)的訪問
利用信號量實現互斥:
semaphore mutex = 1; // 互斥信號量mutex, 初始化為1
P1(){
P(mutex);
訪問臨界區;
V(mutex);
}
P2(){
P(mutex);
訪問臨界區;
V(mutex);
}
生產者-消費者問題
問題本質 : 實現對一個大小為n的緩沖區的互斥訪問, 存取操作
問題描述:
生產者消費者問題(英語:Producer-consumer problem),也稱有限緩沖問題(英語:Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述了兩個共享固定大小緩沖區——即所謂的“生產者”和“消費者”——在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩沖區中,然后重復此過程。與此同時,消費者也在緩沖區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩沖區滿時加入數據,消費者也不會在緩沖區中空時消耗數據。
關鍵點 :
- 生產者消費者共享一個大小為n, 初始為空的緩沖區----緩沖區即臨界資源
- 緩沖區未滿時生產者才可以將產品放入----設置empty信號量
- 緩沖區不為空時消費者才可以將產品取出----設置full信號量
實現 :
信號量設置 :
semaphore mutex = 1; // 互斥信號量, 實現對緩沖區的互斥訪問
semaphore empty = n; // 同步信號量, 表示空閑緩沖區數(可放產品數)
semaphore full = 0; // 同步信號量, 表示非空緩沖區數(放入產品數)
生產者消費者操作:
producer(){ // 生產者
while(1){
P(empty);
P(mutex);
產品放入緩沖區;
V(mutex);
V(full);
}
}
consumer(){ // 消費者
while(1){
P(full);
P(mutex);
取出產品;
V(mutex);
V(empty);
}
}
tips :
- P(mutex)互斥操作必須在同步操作之后, 否則會引發"死鎖":
// 如果改變 P(mutex)和P(empty)順序, 假設此時empty=0,即緩沖區已滿
producer(){ // 生產者
while(1){
P(mutex);
P(empty);
產品放入緩沖區;
V(full);
V(mutex);
}
}
比如我生產者先P(mutex)申請到了臨界資源訪問權限, 但是之后P(empty)時被阻塞, 此時消費者一方又由於mutex被生產者占有而無法取出產品, 導致互相等待對方釋放資源, 即死鎖
- 在此處, 如果緩沖區大小為1, 可以不設置mutex信號量(互斥可以由empty和full滿足)
吸煙者問題
問題本質 : 可以生產多個產品的單生產者問題
問題描述 :
假設一個系統中有三個抽煙者進程,每個抽煙者不斷地卷煙並抽煙。抽煙者卷起並抽掉一顆煙需要有三種材料:煙草、紙和膠水。一個抽煙者有煙草,一個有紙,另一個有膠水。系統中還有兩個供應者進程,它們無限地供應所有三種材料,但每次僅輪流提供三種材料中的兩種。得到缺失的兩種材料的抽煙者在卷起並抽掉一顆煙后會發信號通知供應者,讓它繼續提供另外的兩種材料。這一過程重復進行。
關鍵點 :
-
臨界資源---桌子, 視為緩沖區, 大小為1 ---- 設置同步信號量finish
-
產品有3種不同的組合, 分別給3個不同的人使用 ---- 設置同步信號量offer1, offer2, offer3
-
生產者如何實現輪流生產3種產品 ---- for i in range(3)即可
注意這里不需要設置額外的互斥信號量mutex, 因為緩沖區大小為1
實現 :
信號量設置:
semaphore offer1 = 0, offer2 = 0, offer3 = 0;
semaphore finish = 0; // 抽煙是否完成
int i = 0; // 實現"輪流生產"
provider(){
while(1){
if(i == 0)
V(offer1);
else if(i == 1)
V(offer2);
else if(i == 2)
V(offer3);
i = (i + 1) % 3;
P(finish); // 注意由於finish初值為0,所以將P(finish)放在后面
}
}
smoker1(){
while(1){
P(offer1); // 拿煙,抽了
V(finish); // 完成抽煙,告訴生產者可以繼續生產下一個了
}
}
smoker2(){
while(1){
P(offer2); // 拿煙,抽了
V(finish); // 完成抽煙,告訴生產者可以繼續生產下一個了
}
}
smoker3(){
while(1){
P(offer3); // 拿煙,抽了
V(finish); // 完成抽煙,告訴生產者可以繼續生產下一個了
}
}
讀-寫者問題
問題本質 : 允許多個進程同時讀緩沖區, 但是只允許一個進程寫緩沖區
問題描述 :
一個共享文件, 可以有多個讀者同時讀文件, 或者一個寫着向文件中寫信息, 任一寫者完成寫操作前不允許其他讀 / 寫者工作, 寫者執行寫操作前所有的讀者應當退出
關鍵點 :
- 實現多個讀者同時讀
- 實現讀者-寫者,寫者-寫者之間的互斥
實現 :
方案一
信號量設置 :
semaphore rw = 1; // 實現對文件的互斥訪問
int count = 0; // 記錄讀者的數目
semaphore mutex = 1; // 實現互斥
寫者 :
writer(){
while(1){
P(rw);
write......... // 寫文件
V(rw);
}
}
讀者 :
reader(){
while(1){
P(mutex); // 這里的mutex進用於實現count的互斥, 防止兩個讀者同時進入時出問題
if(count == 0)
P(rw); // 第一個讀者進來時將文件"鎖定"
count++; // 每來一個讀者,count+1
V(mutex);
read........ // 讀文件
P(mutex)
count--;
if(count == 0)
V(rw); // 最后一個讀者退出時將文件權限釋放
V(mutex);
}
}
方案一的問題
仔細分析后, 我們從上述方案中可以發現一個問題, 那就是如果讀者源源不斷的到來, 寫者將一直被掛起"餓死", 這個方案實際上是不公平的, 具有"讀進程優先的特性", 為了解決這個問題, 我們可以引入一個新的信號量w=1, 實現讀者寫者的公平性.
方案二
信號量設置 :
semaphore rw = 1;
int count = 0;
semaphore mutex = 1;
semaphore w = 1; // 方案一基礎上增加
寫者 :
writer(){
while(1){
P(w);
P(rw);
write......... // 寫文件
V(rw);
V(w);
}
}
讀者 :
reader(){
while(1){
P(w);
P(mutex);
if(count == 0)
P(rw);
count++;
V(mutex);
V(w); //注意V(w)放在read之前
read........
P(mutex)
count--;
if(count == 0)
V(rw);
V(mutex);
}
}
分析 : 可以看到, 在方案二中, 如果讀者讀的過程中有寫着想要訪問, 那么該寫者進程將掛在信號量w的等待隊列上, 當該讀者讀完退出后, 寫者即可以寫, 當一個讀者在讀的時候, 它已經V(w)操作了, 也不會影響讀者讀的並行
哲學家就餐問題
問題本質 : 進程需持有多個臨界資源才可以工作, 如何避免分配不當導致"死鎖"
問題描述 :
哲學家就餐問題可以這樣表述,假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時候,他們就停止思考,思考的時候也停止吃東西。餐桌中間有一大碗意大利面,每兩個哲學家之間有一只餐叉。因為用一只餐叉很難吃到意大利面,所以假設哲學家必須用兩只餐叉吃東西。他們只能使用自己左右手邊的那兩只餐叉。哲學家就餐問題有時也用米飯和筷子而不是意大利面和餐叉來描述,因為很明顯,吃米飯必須用兩根筷子。
由於有5只筷子, 相當於5個臨界資源, 我們定義信號量數組chopstick[5]來表示這5個臨界資源
semaphore chopstick[5] = {1,1,1,1,1}
同時為了方便描述, 我們給哲學家和筷子都編號
根據編號有如下定義:
哲學家i 號的左手筷子為chopstick[i] , 右手為chopstick[(i+1)%5]
我們很容易想到一種方式分配臨界資源:
方案一
Pi(){ // Pi表示第i個哲學家進程
while(1){
P(chopstick[i]); // 拿左邊筷子
P(chopstick[(i + 1) % 5]); // 拿右邊筷子
eat...... // 吃飯
V(chopstick[i]); // 放下左邊筷子
V(chopstick[(i + 1) % 5]); // 放下右邊筷子
}
}
這個方案有一個很明顯的問題, 那就是如果五個哲學家同時拿筷子, 那么每個人都將拿起自己左手的筷子而等待右手的筷子 , 也就導致了"死鎖"的局面, 如下圖:
我們可以通過幾種方式改變這種死鎖局面, 核心思想均是防止所有人同時拿到1根筷子 :
方案二
描述 : 限制最多四人同時就餐
semaphore cnt = 4; // 限制4個人
semaphore chopstick[5] = {1,1,1,1,1};
semaphore
Pi(){ // Pi表示第i個哲學家進程
while(1){
P(cnt);
P(chopstick[i]); // 拿左邊筷子
P(chopstick[(i + 1) % 5]); // 拿右邊筷子
eat...... // 吃飯
V(chopstick[i]); // 放下左邊筷子
V(chopstick[(i + 1) % 5]); // 放下右邊筷子
V(cnt);
}
}
方案三
描述 : 奇數號先拿左手的筷子, 偶數號先拿右手的筷子
semaphore chopstick[5] = {1,1,1,1,1};
Pi(){ // Pi表示第i個哲學家進程
while(1){
if(i % 2 == 1){ // 奇數號
P(chopstick[i]); // 拿左邊筷子
P(chopstick[(i + 1) % 5]); // 拿右邊筷子
}
else{ // 偶數號
P(chopstick[(i + 1) % 5]); // 拿右邊筷子
P(chopstick[i]); // 拿左邊筷子
}
eat...... // 吃飯
V(chopstick[i]); // 放下左邊筷子
V(chopstick[(i + 1) % 5]); // 放下右邊筷子
}
}
方案四
描述 : 互斥"拿筷子"這個動作
semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1;
Pi(){ // Pi表示第i個哲學家進程
while(1){
P(mutex); // 互斥
P(chopstick[i]); // 拿左邊筷子
P(chopstick[(i + 1) % 5]); // 拿右邊筷子
V(mutex);
eat...... // 吃飯
V(chopstick[i]); // 放下左邊筷子
V(chopstick[(i + 1) % 5]); // 放下右邊筷子
}
}
四種方法都很好理解, 總而言之就是不讓五個哲學家都陷入等待的局面就ok啦~