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

  在这篇文章中,重点讨论三个经典的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 = 4while(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操作的套路是什么?拿到一个问题我该怎么分析呢?

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

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个使用PV操作解决生产者消费者问题的示例代码(使用Python的multiprocessing库): ```python from multiprocessing import Process, Semaphore, Lock, Queue import time import random # 生产者进程 def producer(items, queue, empty, full, lock): for i in range(items): # 等待空闲位置 empty.acquire() # 添加数据 lock.acquire() queue.put(i) print(f'生产者添加数据 {i}') lock.release() # 释放已经填充的位置 full.release() # 随机等待一段时间 time.sleep(random.uniform(0, 1)) # 消费者进程 def consumer(items, queue, empty, full, lock): for i in range(items): # 等待已经填充的位置 full.acquire() # 取出数据 lock.acquire() data = queue.get() print(f'消费者取出数据 {data}') lock.release() # 释放空闲位置 empty.release() # 随机等待一段时间 time.sleep(random.uniform(0, 1)) if __name__ == '__main__': # 缓冲区大小 n = 5 # 生产者和消费者要处理的数据 items = 10 # 创建共享队列 queue = Queue(maxsize=n) # 创建信号和互斥锁 empty = Semaphore(n) full = Semaphore(0) lock = Lock() # 创建生产者和消费者进程 producer_process = Process(target=producer, args=(items, queue, empty, full, lock)) consumer_process = Process(target=consumer, args=(items, queue, empty, full, lock)) # 启动进程 producer_process.start() consumer_process.start() # 等待进程结束 producer_process.join() consumer_process.join() ``` 在这个示例代码中,我们使用了Python的multiprocessing库来创建生产者和消费者进程,并且使用了Semaphore和Lock对象来实现PV操作。具体来说,我们使用Semaphore对象来表示空闲位置和已经填充的位置的数,使用Lock对象来实现互斥访问共享队列。在生产者进程中,我们首先等待一个空闲位置,然后添加数据并释放一个已经填充的位置;在消费者进程中,我们首先等待一个已经填充的位置,然后取出数据并释放一个空闲位置。通过这种方式,我们可以保证生产者和消费者进程之间的同步,并且避免了数据竞争和死锁等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值