進程同步/互斥問題全解
包含了進程同步的傳統問題,北航操作系統的一些祖傳作業,期末考試題,PPT上的題,一些408統考真題,961專業課真題。搞清楚本文(以及單獨一篇文章中的讀者寫者問題)之后,這進程同步大題考試基本就搞定了。
往下滑一滑,文章右側有目錄可以點擊跳轉。
內容一覽:
- 奇偶數生產者-消費者問題(經典母題)
- 思路:互斥訪問共同存儲空間,P(要用的東西),V(生產出來的東西),V(空閑)
- 野人吃肉(修改了數量的生產者-消費者問題)
- 緩沖區產品(也屬於有限制的生產者-消費者問題)
- 壽司店問題(排隊)
- 搜索-插入-刪除問題
- 哲學家就餐問題
- 進階的哲學家就餐(2019統考)
- 吸煙者問題
- 一定數量差的入倉庫問題
- 面包師叫號賣面包
- 理發店問題(一人銷售)
- 倉庫多產品問題(2017-961統考)
- 上下校車問題(2018-961統考/2020期末考試)
- 2015年961考了讀者寫者問題,詳細解讀請跳轉這篇文章(讀者優先、讀寫公平、寫者優先)
奇偶數生產者-消費者問題
三個進程 P1、P2、P3 互斥使用一個包含 N(N>0)個單元的緩沖區。
P1 每次用 produce()生成一個正整數並用 put()送入緩沖區某一個空單元中;
P2 每次用 getodd()從該緩沖區中取出一個奇數並用 countodd()統計奇數個數;
P3 每次用 geteven()從該緩沖區中取出一個偶數並用 counteven()統計偶數個數。
請用信號量機制實現這三個進程的同步與互斥活動, 並說明所定義的信號量的含義。
答:想象P1為生產者,P2、P3為消費者。
P1、P2共享緩沖區的奇數部分,故設置同步信號量odd;
P1、P3共享緩沖區的偶數部分,故設置同步信號量even;
P1、P2、P3共享整個緩沖區,用一個互斥mutex來保護,再設置同步信號量empty,表示緩沖區空位資源。
偽代碼如下:
#define N 100
Semaphore empty = N; // 假設初始條件下緩沖區有N個空位
Semaphore mutex = 1;
Semaphore odd = 0;
Semaphore even = 0;
void P1(){
int integer;
while(true){
integer = produce(); // 生成一個整數
P(empty); // down(empty),若empty為0則會被阻塞(等待別人拿走)
P(mutex); // 開始互斥,down(mutex)
put(); // 放入緩沖區
V(mutex); // 訪問臨界區結束,up(mutex)
if(integer %2 == 0){
V(even); // 是偶數
} else {
V(odd); // 是奇數
}
}
}
void P2(){
while(true){
P(odd); // 請求一個奇數,down(odd)
P(mutex); // 互斥
getodd();
V(mutex);
V(empty); // 緩沖區多一個位置,up(empty)
countodd();
}
}
void P3(){
while(true){
P(even); // 請求一個偶數,down(even)
P(mutex); // 互斥
geteven();
V(mutex);
V(empty); // 緩沖區多一個位置,up(empty)
counteven();
}
}
野人吃肉
一個野人部落從一個大鍋中一起吃燉肉,這個大鍋一次可以存放 M 人份的燉肉。
當野人們想吃的時候,如果鍋中不空,他們就自助着從大鍋中吃肉。如果大鍋空了,他們就叫醒廚師,等待廚師再做一鍋肉。
答:野人和廚師共享緩沖區鍋,設置互斥信號量mutex;
廚師一次性往鍋里放M份肉,設置廚師信號量cook,整形變量meat記錄剩下的肉數量。
野人沒吃完時,廚師在睡覺,設置野人信號量wild;
當大鍋空的時候,野人不能夠調用 getServingFromPot() ;
僅當大鍋為空的時候,大廚才能夠調用 putServingsInPot()
#define M 100
Semaphore mutex = 1;
Semaphore cook = 0, wild = 0;
int meat = 0;
void wildman(){
while (true){
P(mutex);
if (meat > 0){ // 鍋里還有肉
getServingFromPot();
eat();
meat = meat - 1;
V(mutex); // 吃完一份,釋放緩沖區占用
}
else{
V(wild); // 喚醒睡覺的廚師
P(cook); // 請求一個廚師加肉,若廚師為0則被阻塞(等待煮肉)
V(mutex);
}
}
}
void Cook(){
while (true) {
P(mutex);
if (meat == 0){ // 煮肉ing
putServingsInPot(M);
meat = meat + M;
V(mutex);
V(cook); // 廚師做好了,可以喚醒野人等待進程繼續吃
}
else{
// 鍋里不空,不用煮肉,睡覺(野人為0,沒有人叫他,阻塞)
P(wild);
}
}
}
緩沖區產品(2014-408統考)
系統中有多個生產者進程和消費者進程,共享用一個可以存 1000 個產品的緩沖區(初始為空),當緩沖區為未滿時,生產者進程可以放入一件其生產的產品,否則等待;
當緩沖區為未空時,消費者進程可以取走一件產品,否則等待。
要求一個消費者進程從緩沖區連續取出 10 件產品后,其他消費者進程才可以取產品,請用信號量 P,V 操作實現進程間的互斥和同步,要求寫出完整的過程;並指出所用信號量的含義和初值。
答:生產者可以一直不停地放,除非滿了則阻塞,設置同步信號量empty表示空的數量,同步信號量stuff表示產品的數量;
生產者和消費者共享一個緩沖區,設置互斥信號量mutex_buff來互斥訪問;
有所不同的是,一個消費者必須一次拿1件產品,連續拿10次,讀10次緩沖區,該過程不可中斷。
#define N 1000;
Semaphore mutex = 1; // 連續取10次的"原子操作保護"
Semaphore mutex_buff = 1;// 緩沖區互斥訪問保護
Semaphore empty = N; // 定義緩沖區大小,空位初始為1000
Semaphore stuff = 0;
void producer(){
while(true){
P(empty); // 若empty為0沒有空閑則等待
P(mutex_buff); // 互斥訪問buffer
puts(); // 放入一件產品
V(mutex_buff);
V(stuff); // 釋放產品,可以喚醒消費者等待拿走
}
}
void consumer(){
int i = 0;
while(true){
// 當一個消費者P(mutex)時其他消費者P(mutex)會被阻塞(等待別人拿完)
P(mutex); // 占用緩沖區直到拿走10個再釋放
for(i = 0; i < 10; i++){
P(stuff); // down(stuff),產品不足會被阻塞
P(mutex_buff);// 互斥訪問buffer
gets(); // 取走一件物品
V(mutex_buff);
V(empty); // up(empty)
}
V(mutex); // 10都拿走了,釋放互斥
}
}
生產線裝配車間
某工廠有兩個生產車間和一裝配車間,兩個生產車間分別生產A,B兩種零件,裝配車間的任務是把A、B兩種零件組裝成產品。
兩個生產車間每生產一個零件后,都要分別把它們送到專配車間的貨架F1, F2上。F1存放零件A, F2存放零件B, F1和F2的容量均可存放10個。
裝配工人每次取一個A和一個B零件后組裝成產品,請用PV操作進行正確管理。
答:其實就是多了幾個進程的緩沖區管理。不多解釋,這個是王道上的題....打字手太酸了
不過我習慣用semaphore表示資源的數量,而不是叫“判斷貨架上是否為空”
Semaphore A_buff = 10; // A貨架的空余位置
Semaphore B_buff = 10; // B貨架的空余位置
Semaphore stuff_A = 0; // A貨物的數量
Semaphore stuff_B = 0; // B貨物數量
Semaphore mutex = 1; // 互斥訪問
void A_produce(){
ProduceA();
P(A_buff); // 占用一個A貨架的空間
P(mutex);
Put_to_shelfA();
V(mutex);
V(stuff_A); // 又多了一個A
}
void B_produce(){
ProduceB();
P(B_buff); // 占用一個B貨架的空間
P(mutex);
Put_to_shelfB();
V(mutex);
V(stuff_B); // 又多了一個B
}
void worker(){
P(stuff_A); // 嘗試拿一個A,若無,則被卡在這
P(mutex);
GetA();
V(mutex);
V(A_buff); // 給A貨架空余出來一個位置
P(stuff_B);
P(mutex);
GetB();
V(mutex);
V(B_buff); // 給A貨架空余出來一個位置
// 組裝
}
壽司店問題
假設一個壽司店有 5 個座位,如果你到達的時候有⼀個空座位,你可以立刻就坐。但是如 果你到達的時候 5 個座位都是滿的有⼈已經就坐,這就意味着這些人都是一起來吃飯的,那么你需要等待所有的⼈一起離開才能就坐。編寫同步原語,實現這個場景的約束。
答:
- 使用整型變量
eating
和waiting
記錄在壽司店就餐和等待的線程,使用信號量mutex
對這兩個變量的讀寫進行保護 - 使用信號量
queue
表示排隊 - 使用布爾變量
must_wait
表示壽司店現在滿座,新來的顧客必須等待
int eating = 0, waiting = 0;
Semaphore mutex = 1, queue = 1;
bool must_wait = false;
Customer(){
P(mutex);
if (must_wait){
waiting++;
V(mutex); //對waiting變量的保護可以釋放
P(queue); // 被阻塞,坐着等待排隊,等待被喚醒
}
else {
eating++;
must_wait = (eating == 5)
// 一旦我是最后一個來坐下吃導致人滿的就要等所有人一起吃完,好難過
V(mutex); // 對eating變量的保護可以釋放
}
// 上一部分已經解決了進店后是等待還是吃的問題
Eat_sushi();// else的人和被喚醒的排隊者成功進入這一步
P(mutex); // 開啟對eating, waiting變量保護
eating--; // 吃的人-1,如果5個沒全吃完,不可以換下一批人吃
if (eating == 0){ // 最后一個吃完的人離開才可以進顧客
int n = min(5, waiting); // 放顧客進來的數量,不超過5個
waiting -= n;
eating +=n;
must_wait = (eating == 5)
for(int i = 0; i<n; i++)
V(queue); // 喚醒排隊的n個人繼續進程
}
V(mutex); // 允許下一個吃完的人對變量和隊列進行操作
}
搜索-插入-刪除問題
三個線程對一個單鏈表進行並發的訪問,分別進行搜索、插入和刪除。搜索線程僅僅讀取鏈表,因此多個搜索線程可以並發。
插入線程把數據項插入到鏈表最后的位置;多個插入線程必須互斥防止同時執行插入操作。但是,一個插入線程可以和多個搜索線程並發執行。
最后,刪除線程可以從鏈表中任何一個位置刪除數據。 一次只能有一個刪除線程執行;刪除線程之間,刪除線程和搜索線程,刪除線程和插入線程都不能同時執行。
請編寫三類線程的同步互斥代碼,描述這種三路的分類互斥問題。
- 這個問題與讀者寫者有些類似
Semaphore insertMutex =1, searchMutex = 1; // 保護int變量
Semaphore No_search = 1; // 顧名思義,為1時沒有搜索進程訪問
Semaphore No_insert = 1; // 為1時沒有插入進程訪問
//當上述兩個信號量同時為1,刪除者才可以進行刪除操作
int searcher = 0, inserter = 0;
void Search(){
P(searchMutex);
searcher++;
if (searcher == 1) // 第一個進來的搜索者加鎖
P(No_search)
V(searchMutex);
Searching(); // 訪問臨界區,多個搜索無需互斥
P(searchMutex);
searcher--;
if (searcher == 0)
V(No_search); // 表示此時沒有搜索線程在進行,解鎖
V(searchMutex);
}
void Insert(){
P(insertMutex);
inserter++;
if (inserter == 1)
P(No_insert);
V(insertMutex);
P(insertMutex); // 既然可以和搜索線程並行,那么不用管Searcher
Inserting(); // 訪問臨界區,多個插入者要互斥訪問,一次一個insert
V(insertMutex);
P(insertMutex);
inserter--;
if (inserter == 0)
V(No_insert); // 解鎖,可喚醒刪除者
V(insertMutex);
}
void Delete(){ // 刪除線程與其他任何線程互斥
P(No_search);
P(No_insert); // 若為1則可進入,這個信號量順便也可以當作刪除者的互斥保護
Deleting(); // 搜索和插入線程都沒,成功進入臨界區
V(No_insert);
V(No_search);
}
哲學家進餐問題
一個圓桌有五個哲學家,每兩個人之間一個筷子。
當哲學家餓了才會拿起筷子,一次拿一根,一個人必須同時拿到兩邊的筷子才能吃飯,吃完再放下。
問題的關鍵時如何讓哲學家拿到左右兩根筷子而不造成死鎖或飢餓現象
每個筷子都是互斥訪問的,定義互斥信號量數組chopsticks[5] = {1,1,1,1,1}
哲學家編號為0~4
,每個人左邊的筷子編號是i
,顯然,右邊的筷子編號是(i+1)%5
為了防止所有人都拿左/右手邊的筷子導致死鎖,制定規則:當一名哲學家左右兩邊的筷子都可用時,才能拿起來。(不僅考慮眼前的一步,而且考慮下一步,是哲學家問題的思想精髓)
Semaphore chopsticks[5] = {1,1,1,1,1}
Semaphore mutex = 1;
void Pi(){
do{
P(mutex); // 取筷子前先占用互斥量,每次只有一個人能拿筷子
P(chopsticks[i]); // 拿左邊筷子,后面拿該筷子的人等待
P(chopsticks[(i+1)%5]); // 要能夠也拿起右邊筷子才能吃
V(mutex); // 當一個人拿起兩根筷子,才能讓下一個人拿
Eat();
V(chopsticks[i]); // 放下左邊筷子,使等着該筷子人拿起
V(chopsticks[(i+1)%5]); // 放下右邊筷子
// think
}while(true)
}
進階的哲學家就餐(2019統考)
王道上有。待更新。我絕對不要和哲學家吃飯。
吸煙者問題
一個供應進程,提供三種材料,煙草、紙、膠水,每次隨機提供其中兩種
三個抽煙者進程,這三人分別有煙草、紙、膠水,需要另外兩個東西才能抽完煙
int random;
Semaphore offer1 = 0; // 煙草+紙 組合
Semaphore offer2 = 0; // 煙草+膠水
Semaphore offer3 = 0; // 紙+膠水
Semaphore finish = 0;
void Provider(){ // 供應者進程
while(true){
random = Rand()%3;// 隨機生成0-2
if(random==1)
V(offer1);
else if(random==2)
V(offer2);
else if(random==3)
V(offer3);
// 釋放資源之后等一個人把材料用掉
P(finish);
}
}
void Tobacco(){// 有煙草的人
while(true){
P(offer3);
// smoke
V(finish); //釋放完成信號喚醒供應者
}
}
void Paper(){ // 有紙的人
while(true){
P(offer2);
// smoke
V(finish); //釋放完成信號喚醒供應者
}
}
void Glue(){ // 有膠水的人
while(true){
P(offer1);
// smoke
V(finish); //釋放完成信號喚醒供應者
}
}
一定數量差的入倉庫問題
在一個倉庫中可以存放A和B兩種產品,要求
- 每次只能放入一種產品。
- |A|-|B| < M
- |B|-|A| < N
|A|代表A產品數量。M,N是正整數,用P、V操作描寫產品A和B的入庫過程
#define M 10
#define N 20 // 題目沒有說M N是多少,暫且假設
Semaphore mutex = 1; // 保護倉庫,每次只能放一個產品
Semephore A_surplus = M-1;// A可以比B多的數量(視為資源)最多為M-1個
Semephore B_surplus = N-1;// B可以比A多的數量(視為資源)最多為N-1個
// 顯然,每生產一個A,它可以surplus B的數量就會減少,B對A的順差又多了一個
// 同理,每生產一個B,它與A的數量差就會減1,數量差從N-1開始
void A_Process(){
while(true){
P(A_surplus);
P(mutex);
Put_A();
V(mutex);
V(B_surplus);
}
}
void B_Process(){
while(true){
P(B_surplus);
P(mutex);
Put_B();
V(mutex);
V(A_surplus);
}
}
面包師叫號賣面包
面包師有很多面包,由n名銷售人員推銷,每名顧客進店后取一個號,並且等待叫號,當一名銷售人員空閑則叫下一個號。(有點像銀行櫃台)
設計一個銷售人員和顧客同步的算法.
(王道的答案沒有寫對銷售人員排隊的信號量)
每次顧客來的時候叫號都會現場新生成一個get_num++
,所以只有cur==get_num
的時候就代表已經沒人可叫了
#define N 10
Semaphore mutex = 1; // 保護waiting變量
Semaphore sales = N; // 有N個銷售人員
int cur = 0, get_num = 0; // 當前叫號, 顧客取號
void Salesperson(){
while(true){
P(mutex); // 互斥訪問int變量
if(cur == get_num)
V(mutex);
// 說明當前叫號已經叫到最新的顧客了,等待下一個顧客
else if (cur < get_num){ // 叫號
cur++;
V(mutex);
V(sales); // 表示有銷售空閑了,喚醒對首排隊等待的顧客
Service();
}
}
}
void customer(){
P(mutex);
get_num++; // 取號(先++,顯然號碼從1開始)
V(mutex);
P(sales); // 若sales都沒空的了,則被阻塞等待
// 上面的V(sales)喚醒這里的顧客進程,由等待->買面包
inService();
}
理發店問題(一人銷售)
倉庫多產品問題(2017-961統考)
待更新。
上下校車(2018-961/2020春期末考試)
2018-961真題
假設有n個旅客和1輛車,旅客在汽車停靠的站點反復停車,汽車一次可以乘坐C個旅客(C < n)。汽車坐滿C個旅客后出發繞一圈,回來到原來的站點讓旅客下車。旅客和汽車重復這個過程,二者需要滿足下列條件:
-
旅客能上車和下車
-
汽車能夠載客、運行、卸客
-
只有汽車處於載客狀態后,旅客才能上車。
-
只有C個旅客上車后才能出發
-
只有汽車處於卸客狀態后,旅客才可以下車
-
旅客都下車后,汽車才能重新載客。
【解析】也就是前一批人下完了,最后一個人喚醒司機,讓司機喚醒排隊的第一個人上車,司機
P(full)
等待滿員,然后第一個人上完了又叫后一個人,直到第C個人上車,喚醒司機開車),其實整個結構是很對稱的,有點像讀者寫着占用完釋放的過程
int count = 0; // 用於記錄已經在車上的人數
Semaphore queue = 0; // 排隊上車隊列
Semaphore getoff = 0; // 排隊下車隊列
Semaphore empty = 1; // 若1表示可以載客,用於司機等待下完所有人
Semaphore full = 0; // 用於司機等待上滿客
Semaphore mutex = 0; // 保護count變量
void Passenger(){
while(true){
P(queue); // 來了先排隊
P(mutex); // 此時被喚醒上車
count++;
if(count == C)
V(full); // 若滿座,告訴司機發車
else
V(queue);// 沒滿座,叫下一個人上車
V(mutex);
// 坐在車上,等待發車,以及游覽中
P(getoff); // 等待下車
P(mutex);
count--;
if(count == 0)
V(empty); // 最后一個人下來,告訴司機結束了
else
V(getoff);// 否則叫下一個人下車
V(mutex);
}
}
void Car(){
while(true){
V(queue); // 叫隊首第一個人上車,會引發一個接一個上車
P(full); // 司機等待上車完畢,來喚醒自己
// 發車,游覽中
V(getoff);// 叫第一個人下車,會引發一個接一個下車
P(empty); // 等待最后一個人告訴自己下完了
// 可以進行下一批上客
}
}
2020春
新鮮出爐的題目,跟上面那題不太一樣,上面那題車是循環上下的,司機不用管要上多少個人,只有人滿了才會發車,所以乘客自己數多少人(那不就是沙河的擺渡車嗎!)。而下面這題像公交車,來一次就接人馬上就走。
題目描述:
校車問題:乘客來到校車的停車站等待校車。當巴士到達的時候,所有正在等待的乘客調用 boardBus()上車。*一旦開始上車,任何新到來的乘客都必須等待下一輛巴士。*校車的容量為50人,如果有超過50個人排隊,50名之外的乘客需要等待下一輛巴士。當所有等待的乘客上車完畢,巴士可以離開(調用depart())。如果巴士到達時,沒有任何乘客,它就會立刻離開(調用depart())。
請用PV操作編寫巴士進程和乘客進程的同步互斥關系,滿足上述約束條件。
答:
由題目可知,一旦開始上車(不管是否夠50個人),新來的人也只能等待,所以上車過程是互斥的。用兩個進程來表示乘客和巴士的同步互斥情況。
所以當一個人到來時,必須要排隊等待P(queue):
-
- 若開始上車,則等待下一輛(循環V(queue)時不會輪到他)
- 若沒開始上車,但是前面有50個人,不會輪到他。
- 若沒開始上車,且前面不足50個人,則等待車來了上車。
當巴士到站時:
-
- 若沒有人等待,直接depart()離開
- 若有人等待(≤50人),則等待所有人boardBus()完成后,depart()離開
- 若有人等待(>50人),只上50個,循環V(queue)50次
#define N 50 //一輛車滿載50人
Semaphore mutex = mutex2 = 1;// 用於保護waiting變量
Semaphore board = 1;// 用於保護上車
Semaphore queue = 0;// 用於排隊等待一輛車,初始隊伍0人
int waiting = 0; // 表示等待即將到來的車人數,初始等車的人為0
int should_go_this_time=0;
// 乘客進程
void Passenger(){
P(mutex); // 保護waiting變量
waiting++;// 到來的時候必須要等待
V(mutex);
P(queue); // 占用一個排隊名額,queue是負數時,|queue|=排隊人數
// 此時被下面的bus喚醒,接觸排隊阻塞,繼續
// 上面解決了這個人應該等待
P(mutex2);
boardBus(); // 巴士到達,開始上車
waiting--; // 等待的人減少
should_go_this_time--;
V(mutex2);
// 若上車動作較慢,還不能走
if(should_go_this_time == 0)
V(ready); // 告訴司機可以走了
}
// 巴士進程
void Bus(){
if(waiting == 0)
depart();
else{
// 確定了n之后,新來的同學也不能上車了
P(board); //開始保護上車進程
n = min(N, waiting); // 最多上50個
should_go_this_time = n;
for(int i = 0; i<n; i++){ //開始上車
V(queue); // 喚醒排隊的n個人繼續進程
}
V(board);
P(ready); // 等待所有人上完車
depart(); // 如果waiting==0, n==0, 直接就走了
}
}