文章目录
- 2.3_11 管程
- (一)为什么要引入管程
- (二)管程的定义和基本操作
- (三)拓展1:用管程解决生产者消费者问题
- (四)Java中类似于管程的机制
- 总结
2.3_11 管程
(一)为什么要引入管程
信号量机制存在的问题是——编写程序困难、易出错。
在我们之前分析的那些同步、互斥问题当中,已经体会到了,我们需要非常关心P、V操作的设计、摆放位置。这就造成编写程序困难、易出错的问题。
人们就想,能不能设计一种机制,让程序员写程序的时候不需要再关注复杂的PV操作,让写代码更轻松呢?
1973年,Brinch Hansen首次在程序设计语言(Pascal)中引入了“管程”成分——一种高级同步机制。
注意:管程的作用,和之前的PV操作一样,同样也是用来实现进程的同步、互斥的,只是它更高级。
(二)管程的定义和基本操作
管程也是用来实现进程的互斥、同步的。而进程之间之所以需要实现互斥、同步,是因为进程之间可能会共享某些数据资源。
比如生产者-消费者问题中,生产者、消费者都需要共享地访问缓冲区这种资源。
管程是一种特殊的软件模块,由这些部分组成:
1.局部于管程的共享数据结构说明;
比如生产者-消费者问题当中,“缓冲区”就可以用一个数据结构来说明。
2.对该数据结构进行操作的一组过程;
“一组过程”其实就是“一系列函数”。即,还要给出对刚才的共享数据结构进行操作的一些函数。
3.对局部于管程的共享数据设置初始值的语句;
就是对数据结构当中的值进行初始化。
4.管程的名字。
注意:管程的上述定义,其实就很像面向对象中的“类”。
管程的基本特征:
1.局部于管程的数据只能被局部于管程的过程所访问;
2.一个进程只有通过调用管城内的过程才能进入管程访问共享数据;
3.每次仅允许一个进程在管程内执行某个内部过程。
(三)拓展1:用管程解决生产者消费者问题
我们可以用程序设计语言提供的相关语法,来定义一个管程。
如上图(是一个伪代码),monitor
和end monitor
来定义,中间的部分就是管程。在管程中,我们可以定义一些同步变量,如condition full, empty;
。还可以定义一些普通变量,用来帮助我们处理,如int count = 0;
。在管程中,还定义了两个函数,insert()、remove()
。
这样定义好后,生产者进程如果想要生产,消费者进程如果想要消费,就不用再考虑复杂的PV操作,也不需要考虑同步问题、互斥问题,也不用考虑“缓冲区满了怎么办?缓冲区空了怎么办?”这些问题(管程本身就已经解决好了这些问题),而是直接利用管程,调用对应的函数即可,如下图所示。
引入管程的目的无非就是要更方便地实现进程互斥和同步。
1.需要在管程中定义共享数据(如生产者消费者问题的缓冲区);
2.需要在管程中定义用于访问这些共享数据的“入口”——其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区中取出产品);
3.只有通过这些特定的“入口”才能访问共享数据;
4.管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。)注意:这种互斥特性是由编译器负责实现的,程序员不用关心。
5.可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
程序员可以用某种特殊的语法定义一个管程(比如:monitor ProducerConsumer …… end monitor;
),之后其他程序员就可以使用这个管程提供的特定“入口”很方便地实现进程同步、互斥了。
这也是一种“封装”思想——把一些复杂的细节隐藏了,对外只提供简单易用的接口。
(四)Java中类似于管程的机制
Java中,如果用关键字synchronized
来描述一个函数,那么这个函数同一时间段内只能被一个线程调用。
每次只能有一个线程进入insert
函数,如果多个线程同时调用insert
函数,则后来者需要排队等待。
注:仅作为思维拓展,看不懂也没关系,也不会考。