王道操作系统笔记,视频链接:2.3.6 管程
知识总览
- 管程:
- 为什么要引入管程
- 管程的定义和基本特征
- PS:这部分考试容易考选择题
- 拓展1:用管程解决生产者消费者问题
- 拓展2:Java中类似于管程的机制
为什么要引入管程
- 信号量机制存在的问题:
- 编写程序困难、
- 易出错。
- 比如之前的生产者---消费者问题中,如果实现互斥的P操作在实现同步的P操作之前,就可能导致死锁,如下代码所示,当顺序为①②③执行时,就会发生死锁。
cpp
producer () {
while(1) {
生产一个产品;
P(mutex); ①
P(empty); ②
把产品放入缓冲区;
V(mutex);
V(full);
}
}
consumer () {
while(1) {
P(mutex); ③
P(full); ④
从缓冲区取出一个产品;
V(mutex);
V(empty);
使用产品;
}
}
- 能不能设计一种机制,让程序员写程序时不需要再关注复杂的PV操作,让写代码更轻松呢?
- 1973年,Brinch Hansen 首次在程序设计语言 (Pascal) 中引入了"管程"成分------一种高级同步机制。
- 管程本质上也是用于实现进程的互斥同步的,只不过比信号量更方便易用,是一种更高级的同步机制。
管程的定义和基本特征
- 管程是一种特殊的软件模块,有这些部分组成:
- 局部于管程的共享数据结构 说明;
- 比如生产者和消费者都需要共享一个缓冲区,
- 就可以用一个共享数据结构来表示这个缓冲区
- 对该数据结构进行操作的一组过程 ;
- "过程"其实就是"函数"
- 对局部于管程的共享数据设置初始值的语句;
- 初始化的语句
- 管程有一个名字。
- 管程的定义有些类似类,这里对应类名
- 局部于管程的共享数据结构 说明;
- 管程的基本特征:
- 局部于管程的数据只能被局部于管程的过程所访问;
- 类比public函数修改private属性
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据;
- 类比接口
- 每次仅允许一个进程在管程内执行某个内部过程 。
- PS:(来自deepseek)你可以毫不犹豫地把"管程实例"看作是临界资源。
- 局部于管程的数据只能被局部于管程的过程所访问;
拓展1:用管程解决生产者---消费者问题
- 伪代码:
cpp
monitor ProducerConsumer // 下面就是管程的内容
condition full, empty; //条件变量用来实现同步(排队)
int count=0; //缓冲区中的产品数
// 其实还要定义一些对缓冲区进行描述的一些数据结构
// 比如缓冲区大小、用于存储Item的数组、写入位置的指针、读取位置的指针等
void insert (Item item) { //把产品item放入缓冲区
if (count == N)
wait (full);
count++;
insert_item (item);
if (count == 1)
signal(empty);
}
Item remove () { //从缓冲区中取出一个产品
if (count == 0)
wait (empty);
count--;
if (count == N-1)
signal(full);
return remove_item();
}
end monitor;
//生产者进程
producer (){
while(1){
item = 生产一个产品;
ProducerConsumer.insert (item); // 替代了之前一系列容易混淆的P、V操作
}
}
//消费者进程
consumer (){
while(1){
item = ProducerConsumer.remove (); // 替代了之前一系列容易混淆的P、V操作
// 关于队列空了怎么办这种麻烦问题
// 全部都在管程中解决即可
消费产品item;
}
}
- 由编译器负责实现各进程互斥地进入管程 中的过程。
- 例子:两个生产者进程并发执行,依次调用了insert 过程:
- 所以第一个进程在调用insert函数的时候,是可以顺利执行下去的,
- 如果在第一个进程没有完成执行,第二个进程尝试着调用insert函数,
- 由于编译器实现的这些功能会暂时阻止第二个进程进入insert函数,
- 就会把第二个进程阻塞在insert函数后面,类似一个排队器
- PS:普通的类是没有这个功能的
- 例子:两个生产者进程并发执行,依次调用了insert 过程:
- 管程中设置条件变量和等待/唤醒操作,以解决同步问题
- 例子:两个消费者进程先执行,生产者进程后执行:
- 第一个消费者进程在执行的时候,
- 首先是调用了管程的remove的过程(或者说是函数),
- 此时缓冲区里没有可用的产品(初始count=0),
- 所以第一个消费者进程需要执行wait等待的操作,
- 等待在empty这个条件变量的等待队列当中,
- 同理,第二个消费者进程也会进入empty的等待队列,
- PS:第一个在前,第二个在后
- 生产者进程由于产生的产品是第一个产品,所以会有signal操作,
- 用于唤醒在empty变量的等待队列中的某一个进程,
- 一般来说是队头的进程,也就是第一个消费者进程
- 第一个消费者进程进入,如果发现消费后剩余N-1个产品,
- 也就是消费前产品满了,此时可能有生产者进程阻塞在full变量的等待队列
- 就会去唤醒full变量的等待队列中的某一个进程,
- 原理和之前的empty变量的例子差不多,但是这个例题中肯定不会唤醒的。
- 因为最开始就是一个产品都没有,所以不会有阻塞的生产者进程
- 例子:两个消费者进程先执行,生产者进程后执行:
- 总结:引入管程的目的无非就是要更方便地实现进程互斥和同步 。
- 需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
- 需要在管程中定义用于访问这些共享数据的"入口"------其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
- 只有通过这些特定的"入口"才能访问共享数据
- 管程中有很多"入口",但是每次只能开放其中一个"入口" ,并且只能让一个进程或线程进入 (如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意:这种互斥特性是由编译器负责实现的,程序员不用关心)
- 可在管程中设置条件变量 及等待/唤醒操作 以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出"入口");可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
- 程序员可以用某种特殊的语法定义一个管程 (比如:monitor ProducerConsumer ... end monitor;),之后其他程序员就可以使用这个管程提供的特定"入口"很方便地使用实现进程同步/互斥了。
- 这就是"封装"思想
拓展2:Java中类似于管程的机制
- Java 中,如果用关键字 synchronized 来描述一个函数,那么这个函数同一时间段内只能被一个线程调用
- 代码:
java
static class monitor {
private Item buffer[] = new Item[N];
private int count = 0;
// 这里的synchronized关键字代表该函数同一时间内只能被一个线程所用
// 如果多个线程同时调用insert函数,则后来者需要排队等待
public synchronized void insert (Item item) {
......
}
}
- PS: 不熟悉 Java 看不懂也没关系,不会考,仅作为思维拓展。熟悉 Java 的,在时间充裕的情况下可以动手尝试用 synchronized 实现生产者消费者问题的"管程"。
- 这里只是说明管程机制不仅在操作系统可以用,平常编程也可能用到类似机制
知识回顾与重要考点
