进程同步/互斥问题全解

进程同步/互斥问题全解

包含了进程同步的传统问题,北航操作系统的一些祖传作业,期末考试题,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 个座位都是满的有⼈已经就坐,这就意味着这些人都是一起来吃饭的,那么你需要等待所有的⼈一起离开才能就坐。编写同步原语,实现这个场景的约束。

答:

  • 使用整型变量eatingwaiting记录在寿司店就餐和等待的线程,使用信号量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两种产品,要求

  1. 每次只能放入一种产品。
  2. |A|-|B| < M
  3. |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):

    1. 若开始上车,则等待下一辆(循环V(queue)时不会轮到他)
    2. 若没开始上车,但是前面有50个人,不会轮到他。
    3. 若没开始上车,且前面不足50个人,则等待车来了上车。

当巴士到站时:

    1. 若没有人等待,直接depart()离开
    2. 若有人等待(≤50人),则等待所有人boardBus()完成后,depart()离开
    3. 若有人等待(>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, 直接就走了
  }
}
posted @ 2020-04-25 14:57  Vanellope  阅读(2870)  评论(0编辑  收藏  举报