信号量应用(PV操作)——经典PV操作

  在这篇文章中,重点讨论三个经典的PV操作例题:生产者消费者问题、读者写者问题、哲学家进餐问题。对这三个问题会逐层分析,不断改进。希望能通过这个过程对于PV操作有着更深刻的理解。

  生产者消费者问题

  背景描述:有两类进程(生产者,消费者),生产者负责生产,生产后的产品会放到共享缓冲区内。共享缓冲区的大小为n。只要产品个数没有达到n就可以继续放。消费者负责从共享缓冲区内拿产品,只有共享缓冲区不为空,就可以一直拿。实现两个进程的协调工作。问题重点如下图:
信号量应用(PV操作)——经典PV操作

  1.0版本:

void producer() {
	while (true) {
		produce_an_item;
		while (count == n);
		buffer[in] = item;
		in = (in+1)%n;
		count++;
	}
}  
void customer() {
	while (count == 0);
	item = buffer[out];
	out = (out+1)%n;
	count--;
	consume_the_item;
}

  这个版本中这样一个问题:对于多进程修改的全局变量,为防止在汇编的时候执行顺序出现错误。都会加一个信号量对其进行限制。改进如下:

   1.1版本:

semaphore mutex = 1; 
void producer() {
	while (true) {
		produce_an_item;
		while (count == n);
		buffer[in] = item;
		in = (in+1)%n;
		wait(mutex);
		count++;
		signal(mutex);
	}
}  
void customer() {
	while (count == 0);
	item = buffer[out];
	out = (out+1)%n;
	wait(mutex);
	count--;
	signal(mutex);
	consume_the_item;
}

  1.1中存在问题:使用while循环,存在“忙等”问题,需要用信号量机制加以改善

  2.0版本:

   对于生产者:定义信号量empty,初始化为n,放入产品时,empty的个数减一
   对于消费者:定义信号量full,初始化为0,消耗产品时,full的个数减一
  代码如下:

semaphore empty=n,full=0; 
void producer() {
	while (true) {
		produce_an_item;
		wait(empty);
		buffer[in] = item;
		in = (in+1)%n;
		signal(full);
	}
}  
void customer() {
	wait(full);
	item = buffer[out];
	out = (out+1)%n;
	signal(empty);
	consume_the_item;
}

  这个版本中还留下这样一个问题(只针对于多个生产者/消费者):
   问题二:生产者中buffer[in] = item和in=(in+1)%n两句分离。多个生产者进程的情况下,可能将进程A将item1存入当前in中,没有来得及对in进行更改就换到了进程B,进程B将item2又放入in中,覆盖了之前的数据。同理消费者进程的out也是。

   3.0版本

   改进:针对于in/out专门增加一个互斥信号量,用来使两条语句连续执行
   代码如下(consumer同理,在wait(full)下面增加wait(mutex1),在signal(empty)下面增加signal(mutex1)):

semaphore empty=n,full=0,mutex=1; 
void producer() {
	while (true) {
		produce_an_item;
		wait(empty);
		wait(mutex);
		buffer[in] = item;
		in = (in+1)%n;
		signal(mutex);
		signal(full);
	}
}  
void customer() {
	wait(full);
	wait(mutex);
	item = buffer[out];
	out = (out+1)%n;
	signal(mutex);
	signal(empty);
	consume_the_item;
}

   读者写者问题

   问题描述:对于一份文件,操作的对象有两种(读者、写者)。写者可以读也可以写,读者只能读不能写。多个读者可以同时读。写者修改数据时,其他写者和读者不能访问。如下图:
信号量应用(PV操作)——经典PV操作
   问题分析:需要关注顺序问题!
   不可以:写写、读写
   可以:读读、写读

   1.0版本:

semaphore mutex = 1;
void writer() {
	while (true) {
		wait(mutex);
		// write something;
		signal(mutex);
	}
} 
void reader() {
	while (true) {
		wait(mutex);
		// read something;
		signal(mutex);
	}
} 

   这个版本的问题是由于只有一个信号量mutex,导致同时只能有一个读者去读违背了多个读者同时读的要求。

   2.0版本:

semaphore mutex = 1;
int readcount = 0;
void writer() {
	while (true) {
		wait(mutex);
		// write something;
		signal(mutex);
	}
} 

void reader() {
	while (true) {
		if (readcount == 0) {
		wait(mutex);
		readcount++;
		}
		else {
			readcount++;
		}
		// read something
		readcount--;
		if (readcount == 0) {
			signal(mutex);
		}
	} 
}

   改进:该版本设置变量readcount来控制读者数量,只有在readcount为0的时候才加以限制。在reader中无论readcount是否为0,readcount++都执行了。所以简化代码,写成下面这版

   2.1版本:

semaphore mutex = 1;
int readcount = 0;
void writer() {
	while (true) {
		wait(mutex);
		// write something;
		signal(mutex);
	}
} 

void reader() {
	while (true) {
		if (readcount == 0) {
			wait(mutex);
		}
		readcount++;
		// read something
		readcount--;
		if (readcount == 0) {
			signal(mutex);
		}
	}
}

   这个版本还是存在一个问题:readcount作为全局变量,有多处可以改变该值。为了避免混乱,增加信号量对readcount加以限制(对吗?更明确的表达是什么?)

   3.0版本:

semaphore wmutex = 1;
semaphore rmutex = 1; 
int readcount = 0;
void writer() {
	while (true) {
		wait(wmutex);
		// write something;
		signal(wmutex);
	}
} 

void reader() {
	while (1) {
		if (readcount == 0) {
			wait(wmutex);
		}
		wait(rmutex);
		readcount++;
		signal(rmutex);
		// read something
		wait(rmutex);
		readcount--;
		signal(rmutex);
		if (readcount == 0) {
			signal(wmutex);
		}
	}
}

  改进:增加信号量rmutex对readcount进行限制
   3.0版本解决了readcount的混乱问题,但如果多个读者进程的话,假设每个读进程只执行到wait(wmutex)之后,这样readcount始终为0,而wmutex却不断自减,会出现问题。

   4.0版本:

semaphore wmutex = 1;
semaphore rmutex = 1; 
int readcount = 0;
void writer() {
	while (true) {
		wait(wmutex);
		// write something;
		signal(wmutex);
	}
} 

void reader() {
	while (1) {
		wait(rmutex);
		if (readcount == 0) {
			wait(wmutex);
		}
		readcount++;
		signal(rmutex);
		// read something
		wait(rmutex);
		readcount--;
		if (readcount == 0) {
			signal(wmutex);
		}	
		signal(rmutex);
	}
}

  改进:通过将rmutex提前,让对readcount的判断和对readcount的操作变成一个整体。解决了wmutex的不断自减问题。
   但这个版本还是有一个问题:对于写者来说,只有reaccount重新归零后,才能够signal(wmutex)进而执行写进程。如果读者进程特别多的话,readcount始终不为0,写进程始终得不到资源,称这样的问题叫做“写者饿死”。

   5.0版本(解决写者饿死问题)

   改进:增加读写优先级,设置写优先级更高。添加信号量w进行控制。具体代码如下:

semaphore wmutex = 1;
semaphore rmutex = 1; 
w = 1 //写者读优先 
int readcount = 0;
void writer() {
	while (true) {
		wait(w);
		wait(wmutex);
		// write something;
		signal(wmutex);
		signal(w);
	}
} 

void reader() {
	while (1) {
		wait(w);
		wait(rmutex);
		if (readcount == 0) {
			wait(wmutex);
		}
		readcount++;
		signal(rmutex);
		signal(w);
		// read something
		wait(rmutex);
		readcount--;
		if (readcount == 0) {
			signal(wmutex);
		}	
		signal(rmutex);
	}
}

  样例分析:假设进程到达顺序如下:读者A,写者B,读者C
  读者A率先进入,对w,rmutex信号量进行占有。B到来时因为没有释放w而阻塞。A可一直执行至signal(w)处。释放w后B对w进行占有,但在wait(wmutex)处等待。C到来时因w被占用而在wait(w)处等待,直至A执行完毕且因readcount为0,释放wmutex。B得到wmutex进行写操作,写完成后释放w,C才可能读。完成B优先于C执行,解决了写者饿死问题。

   哲学家进餐问题

   问题描述如下图:
信号量应用(PV操作)——经典PV操作

   1.0版本(全拿一边:死锁)

semaphore chopstick[5] = {1,1,1,1,1};
void process(int i) {
	while (true) {
		wait(chopstick[i]);
		wait(chopstick[(i+1)%5]);
		eat;
		signal(chopstick[i]);
		siganl(chopstick[(i+1)%5]);
	}
} 

  这个版本代码的思路是先拿一边,再拿另一边。当进餐结束后,按照同样的顺序放下筷子。如果同时有五个进程,都执行了wait(chopstick[i]),之后每根筷子都被占用,所有进程都会停在wait(chopstick[(i+1)%5])。造成死锁。

   2.0版本:

   对于这个问题,主要的思想就是让至少一位哲学家能够拿到两只筷子。当他完成用餐后能够把筷子给其他人,也就是成功的周转开来。有几种解决方案,下面依次介绍下:

   方案一:限制用餐人数

   思路:通过设置信号量数量为4,规定只有4个哲学家能够进餐。这样就会保证至少有一位哲学家能够有两只筷子,当这位哲学家完成后给其他人,保证进程的有序运行。代码如下:

semaphore chopstick[5] = {1,1,1,1,1};
semaphore num_mutex = 4;
while(wait(num_mutex))
{
	wait(chopstick[i]);
	wait(chopstick[(i+1)%5]);
 	// eat()
	
	signal(chopstick[i]);
	signal(chopstick[(i+1)%5]);
	signal(num_mutex);
}
  方案二:使用AND型信号量,当某人同时拿到两个筷子时可以用餐。代码如下:
semaphore chopstick[5] = {1,1,1,1,1}
do{
···
	// think()
	swait(chopstick[(i+1)%5],chopstick[i]);


	//eat()
	signal(chopstick[(i+1)%5],chopstick[i]);

}while(true)
  方案三:对进程按进程奇偶区分,规定奇数进程先拿左边的筷子再拿右边的筷子,偶数进程先拿右边的筷子再拿左边的筷子。这样1,2会竞争1号筷子,3,4会产生竞争3号筷子,5拿到5号筷子。当转向另一只的时候,总会有一个进程拿到两只完成操作。画个图就是这样:

信号量应用(PV操作)——经典PV操作
   代码如下:

semaphore chopstick[5]={1,1,1,1,1};
void philosopher(int i)
{
	while(true)
	{
		think();
		if(i%2 == 0) //偶数哲学家,先右后左。
		{
			wait (chopstick[(i + 1)%5]) ;
			wait (chopstick[i]) ;
			eat();
			signal (chopstick[(i + 1)%5]) ;
			signal (chopstick[i]) ;
		}
		else //奇数哲学家,先左后右。
		{
			wait (chopstick[i]) ;
			wait (chopstick[(i + 1)%5]) ;
			eat();
			signal (chopstick[i]) ;
			signal (chopstick[(i + 1)%5]) ;
		}
	}
}

  参考博客:哲学家就餐问题的解决方案(三种)_jdq8576的博客-CSDN博客_哲学家进餐问题3种代码

  有个问题一直困扰着我:PV操作的套路是什么?拿到一个问题我该怎么分析呢?

  其实到现在我也没有一个确切的答案,只有一点儿小感悟。
  首先应当分析进程,确定有几个进程,进程间关系是什么?
  第二步确定资源是什么?
  第三步,确定信号量的个数。这个很模糊,只能一个一个试,一个不行两个,依次类推。
  另外目前做的题还比较少,等如果以后有了更清晰的想法,会再来补充的!

上一篇:操作系统第6次实验报告:使用信号量解决进程互斥访问


下一篇:哲学家进餐问题