在这篇文章中,重点讨论三个经典的PV操作例题:生产者消费者问题、读者写者问题、哲学家进餐问题。对这三个问题会逐层分析,不断改进。希望能通过这个过程对于PV操作有着更深刻的理解。
生产者消费者问题
背景描述:有两类进程(生产者,消费者),生产者负责生产,生产后的产品会放到共享缓冲区内。共享缓冲区的大小为n。只要产品个数没有达到n就可以继续放。消费者负责从共享缓冲区内拿产品,只有共享缓冲区不为空,就可以一直拿。实现两个进程的协调工作。问题重点如下图:
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;
}
读者写者问题
问题描述:对于一份文件,操作的对象有两种(读者、写者)。写者可以读也可以写,读者只能读不能写。多个读者可以同时读。写者修改数据时,其他写者和读者不能访问。如下图:
问题分析:需要关注顺序问题!
不可以:写写、读写
可以:读读、写读
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执行,解决了写者饿死问题。
哲学家进餐问题
问题描述如下图:
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号筷子。当转向另一只的时候,总会有一个进程拿到两只完成操作。画个图就是这样:
代码如下:
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操作的套路是什么?拿到一个问题我该怎么分析呢?
其实到现在我也没有一个确切的答案,只有一点儿小感悟。
首先应当分析进程,确定有几个进程,进程间关系是什么?
第二步确定资源是什么?
第三步,确定信号量的个数。这个很模糊,只能一个一个试,一个不行两个,依次类推。
另外目前做的题还比较少,等如果以后有了更清晰的想法,会再来补充的!