管程
为什么会提出管程
由于采用信号量及P、V同步机制来编写并发程序,对于共享变量及信号量变量的操作将被 分散于各个进程中,有以下缺点。
(1)程序易读性差,因为要了解对于一组共享变量及信号量的操作是否正确,则必须通读整个系统或者并发程序。
(2)程序不利于修改和维护,因为程序的局部性很差,所以任一组变量或一段代码的修改都可能影响全局。
(3)正确性难以保证,因为操作系统或并发程序通常很大,要保证这样一个复杂的系统没有逻辑错误是很难的。
(4)同步操作使用不当可能导致死锁。
为了更易于编写正确的程序,Brinch Hanson和Hoare提出了一种高级同步机制,称为管程(Monitor)
管程的定义
代表共享资源的数据结构和对该共享数据结构实施操作的一组过程所组成的资源管理程序。
-
管程的三个主要特征:
(1)模块化,一个管程是一个基本程序单位,可以单独编译。
(2)抽象数据类型,管程是一种特殊的数据类型,其中不仅有数据,而且有对数据进行 操作的代码。,这点有点像面向对象编程语言中的类。
(3)信息隐蔽,管程是半透明的,管程中的外部过程函数)实现了某些功能,管程中的 外部过程(函数)实现了某些功能,至于这些功能是怎样实现的,在其外部则是不可见的。
(4)使用的互斥性,任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。
管程有一个很重要的特性,即任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互斥。
管程是编程语言的组成部分,编译器知道它们的特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用。
典型的处理方法是,当一个进程调用管程过程时,该过程中的前几条指令将检查在管程中是否有其他的活跃进程。如果有,调用进程将被挂起,直到另一个进程离开管程将其唤醒。如果没有活跃进程在使用管程,则该调用进程可以进人。
进人管程时的互斥由编译器负责,但通常的做法是用一个互斥量或二元信号量。 因为是由编译器而非程序员来安排互斥,所以出错的可能性要小得多。
在任一时刻,编写管 程的人无须关心编译器是如何实现互斥的,他只需知道将所有的临界区转换成管程过程即 可,绝不会有两个进程同时执行临界区中的代码。
管程的组成
一个管程由四个部分组成。它们是管程名称、共享数据的说明、对数据进行操作的一组过程和对共享数据赋初值的语句。管程能保障共享资源的互斥执行,即一次只能有一个进程 可以在管程内活动。该性能是由管程本身实现的。因此,程序员可以不必显式地编写程序代码去实现这种同步制约。
type monitor_name = monitor;
<共享变量说明>;
define <(能被其他模块引用的)过程名列表>;
use <(要调用的本模块外定义的)过程名列表>;
procedure <过程名>(<形式参数表>);
begin
…
end;
function <函数名>(<形式参数表>):值类型;
begin
…
end;
…
begin
<管程的局部数据初始化语句序列>;
end
-
存在问题
当一个进程调用了管程,在管程中时被阻塞或挂起,进程将处于空等状态,直到阻塞或挂起的原因解除,而在此期间,其它进程无法进入管程,被迫长时间地等待。
-
解决办法:
引入条件变量condition。通常,一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中要设置多个条件变量。
条件变量
条件变量是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供两个操作x.wait和x.signal:
① x.wait:正在调用管程的进程因x条件需要被阻塞或挂起,则调用x.wait将自己插入到x条件的等待队列上,并释放管程,直到x条件变化。此时其它进程可以使用该管程。
②x.signal:正在调用管程的进程发现x条件发生了变化,则调用x.signal,重新启动一个因x条件而阻塞或挂起的进程。
说明:
- 条件变量(用于控制进程阻塞和唤醒)
- 与之前的信号量变量不同,条件变量不取具体值,条件变量相当于每个阻塞队列的队列指针。
- 对条件变量的操作需结合对普通变量的条件判断,从而控制进程状态。
- 条件变量的signal操作与信号量变量的signal操作也不同。条件变量的signal操作,是重新启动一个被阻塞的进程,但如果没有进程被阻塞,则不产生任何效果 。而信号量机制中的signal操作,若没有需要唤醒的进程,必须要做的还有修改信号量变量的值。
说明:
条件队列是wait()操作后被阻塞的进程所形成的队列,而紧急等待队列是signal()后,被释放的进程,相当于就绪状态
signal操作后管理互斥的保证
假定进程P执行x.signal()操作,而条件变量x队列中存在阻塞进程Q等等,则当P执行x.signal()操作后,进程Q被释放。为避免管程中同时有两个进程在执行,可采用如下处理策略之一:
- P等待Q继续,直到Q退出或等待 (Hoare提出的)
- Q等待P继续,直到P等待或退出;
- 规定唤醒为管程中最后一个可执行的操作。(hanson提出的)
管程的优点
- 保证进程互斥地访问共享变量,并方便地阻塞和唤醒进程。管程可以以函数库的形式实现。相比之下,管程比信号量好控制。
- 管程可增强模块的独立性:系统按资源管理的观点分解成若干模块,用数据表示抽象系统资源,使同步操作相对集中,从而增加了模块的相对独立性
- 引入管程可提高代码的可读性,便于修改和维护,正确性易于保证:采用集中式同步机制。一个操作系统或并发程序由若干个这样的模块所构成,一个模块通常较短,模块之间关系清晰。
管程的缺点
大多数常用的编程语言中没有实现管程,如果某种语言本身不支持管程,那么加入管程是很困难的。 虽然大多数编程语言也没有实现信号量,但可将P、V操作作为一个独立的子例程或操作系统的管理程序调用加入。