进程管理(进程的同步与互斥三)

  • 管程机制

虽然信号量及其P、V操作是一种既方便又有效的进程同步工具,但如果采用这种同步机制来编写并发程序,对于共享变量及信号量变量的操作将被分散与各个进程中,有如下缺点:

a.程序易读性差。因为要了解对于一组共享变量及信号量的操作是否正确,则必须通读整个系统或者并发程序。

b.不利于修改和维护。因为程序的局部性很差,所以任一组变量或一段代码的修改都可能影响全局。

c.正确性难以保证。因为操作系统或并发程序通常很大,要保证这样一个复杂的系统没有逻辑错误是很难的。

在解决上述问题的过程中,产生了一种新的进程同步工具-管程。

(1)定义:

把所有进程对某一种临界资源的同步操作都集中起来,构成一个所谓的“秘书”进程。凡要访问该临界区的进程,都需先报告“秘书”,再由“秘书”来实现各个进程对同一临界资源的互斥使用。

一个管程定义了一个数据结构和在该数据结果上能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。

管程是由若干公共变量及其说明和所有访问这些变量的过程所组成的。

管程在结构上由三部分组成:

a.管程所管理的共享数据结构(局部变量和条件变量),这些数据结构是对相应临界资源的抽象。

b.建立在该数据机构上的一组操作(函数)。

c.对上述数据结构置初值的语句。

语法如下:

Monitor monitor-name;	/*管程名字*/
	/*以下为局部变量和条件变量说明
	define 本管程内所定义、本管程外可调用的过程(函数)名字表
	use 本管程外所定义、本管程内将调用的过程(函数)名字表*/
  void Entry P1(...){...}	/*对数据结构进行操作的函数*/
  void Entry P2(...){...}
  ...
  void Entry Pn(...){...}
  {
	Initialization;	/*设置初始值的语句*/
  }

在上述定义中,管程管理的数据结构仅能由管程内定义的函数所访问,而不能由管程外的函数访问。管程中定义的函数又分为两种类型:一是外部函数(带有标识符Entry);二是内部函数(不含Entry标识符)。外部函数是进程可以从外部调用的函数,而内部函数是只能由管程内的函数调用的函数。整个管程的功能相当于“一道墙”,它把共享变量所代表的资源和对它进行操作的若干函数围了起来,所有进程要访问临界资源,都必须经过管程这道“门”才能进入,而管程每次只允许一个进程进入,即便它们调用的是管程中不同的函数。以此自动的实现临界资源在不同进程间的互斥使用。那些要求进入管程但因互斥原因而暂时不能进入的进程需要排队等待。由于管程是一个语言成分,所以管程的互斥访问完全由编译程序在编译时自动添加上,无须程序员关心,而且保证正确。

(2)条件变量:

管程通过防止对一个共享资源的并发访问,达到实现临界区的效果,从而实现了进程互斥。实现进程同步需要引入同步机制,是进程在资源不能满足无法继续执行的情况下被阻塞,同时还不需要开放管程。管程使用称为条件变量的同步机制,让等待的进程临时放弃管程的控制权,然后在适当的时刻,再尝试监测管程内状态的变化。

因为管程是互斥进入的,所以当一个进程试图进入一个已被占用的管程时,它应当在管程的入口处等待,因而在管程的入口处应当有一个进程等待队列,称为入口等待队列。

条件变量是当调用管程过程的进程无法运行时,用于阻塞进程的信号量。为了区别各种不同等待原因,在管程内设置若干条件变量,局限于管程,并仅能从管程内进行访问VAR C:condition;对于条件型变量C,可以执行wait和signal操作。

wait(C)。当一个管程过程发现无法继续时(如发现没有可用资源),它在某些条件变量上执行wait,这个动作引起调用进程阻塞。

signal(C)。指定条件变量上的一个进程被释放。

注:wait和signal虽然和P、V原语操作类似,但它们还是有明显区别的。signal操作是重新启动一个被阻塞的进程,但如果没有进程被阻塞,则操作不产生任何后果,这就与信号量的V操作不同,因为,V操作总是要执行S.value++操作,因而总会改变信号量的值。

当一个进入管程的进程执行wait操作时,它应当释放管程的互斥权;当使用signal释放一个等待进程时(如P唤醒Q),可能出现两个进程同时停留在管程内,怎样决定哪个进程执行、哪个进程等待,处理方法有两种:P等待Q继续,直到Q退出或等待;Q等待P继续,直到P退出或等待。

(3)基本思想:

通过管程中过程的互斥调用解决进程的互斥,通过条件变量上的wait、signal操作解决进程的同步。

正确的管程实现机制必须满足:当一个进程进入管程时,它封锁管程,不允许其他进程再进入;当进程离开管程时,它开放管程,允许其他进程进入;当一个进程在管程内时因为进程同步的要求而挂起时(wait操作),它必须在挂起之前开放管程,允许其他进程进入;当一个进程使用了signal释放另一个进程时,为避免两个进程同时在管程之内,它必须确定哪个进程等待,哪个进程执行。

(4)生产者-消费者问题:

注:本例只有两个进程,一个生产者进程,一个消费者进程,不考虑出现多个进程的情况。

建立一个管程,命名为Producer-Consumer(简称PC管程),其中包含两个外部函数:

a.put(item)函数。生产者进程利用该函数将自己生产的“产品”放入缓冲区中的一个缓冲单元内,并用变量count计数在缓冲区中已有的“产品”数量,当count>=n时,表示缓冲区已满,生产者需等待。

b.get(item)函数。消费者进程利用该过程从缓冲区中的某个缓冲单元取得一个“产品”,当count<=0时,表示缓冲池已空,无“产品”可供消费,消费者应等待。

PC管程定义如下:

Monitor PC;
	int in,out,count;
	item buffer[n];
	condition notfull,notempty;
	void Entry put(item){
		if count >= n then notfull.wait;	/*缓冲区满,进入等待队列*/
		buffer[in] = item;
		in = (in + 1) mod n;
		count++;
		if notempty.queue then notempty.signal;	/*如果有进程(消费者进程)因为缓冲区空而等待,唤醒它*/
	}
	void Entry get(item){
		if count <= 0 then notempty.wait;	/*缓冲区空,进入等待队列*/
		item = buffer[out];
		out = (out + 1) mod n;
		count--;
		if notfull.queue then notfull.signal;	/*如果有进程(生产者进程)因为缓冲区满而等待,唤醒它*/
	}
	{
		in = out = count = 0;
	}
	

有了PC管程的定义之后,生产者-消费者问题的解可描述如下:

cobegin
		void producer(){
			while(true){
				produce an item in nextp;
				PC.put(nextp);
			}
		}
		void consumer(){
			while(true){
				PC.get(nextc);
				consume the item in nextc;
			}
		}
	coend

猜你喜欢

转载自blog.csdn.net/Dream_Ryoma/article/details/81660761