1.为什么要引入管程
我们在日常使用信号量机制时编写程序困难还容易出错,因为会有大量的PV操作散落在各个进程当中,顺序不易管理,容易发生死锁。
1973年,Brinch Hansen首次在程序设计语言(Pascal)中引入了“管程”成分——一种高级的同步机制
目的:
- 把分散在各个进程中的临界区集中管理
- 防止进程的非法同步操作
- 便于编程
管程是由局限于自己的若干共享变量(数据结构)及其说明,和所有访问这些共享变量的过程所组成的软件模块。
基本思想:
把信号量及其操作原语封装在一个对象中,即将共享资源及其操作集中在一个模块中。管程可以使用函数库的形式实现,众多语言中都已经实现了管程。
简单地说管程就是一个概念模型,任何语言都可以实现
2.管程的定义和基本特征
管程的组成结构:
- 局部共享变量和条件变量组成管程内的数据结构;
- 对数据结构进行操作的一组过程 (“过程”可以理解为“函数”)
- 对数据结构进行初始化的语句
- 管程有一个名字
管程的基本特征:
-
局限于管程的共享变量(数据结构)只能被管程中的过程访问;
-
一个进程通过调用管程中的任一过程进入管程;
-
任何时候只能有一个进程在管程中执行,调用管程的其他进程将被挂起,以等待管程变为可用,即管程有效地实现互斥。进入管程的互斥机制由编译器负责,通常使用信号量
-
管程可被系统范围内的进程互斥访问,属于共享资源
monitor ProducerConsumer
condition full, empty;//条件变量用来实现同步
int count = 0 ;//缓冲区中的产品数
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 = 生产一个产品;
ProdecerConsumer.insert(item);
}
}
//消费者进程
consumer(){
while(1){
item = ProdecerConsumer.remove();
消费产品item;
}
}
根据代码我们可以看出生产者生产的产品与消费者消费的产品作为参数传进管程当中,比PV操作简单了许多,缓冲区空了与互斥都不用生产者与消费者担心,都是管程需要解决的问题,有编译器负责实现各个进程互斥地进入管程中的过程,每次仅允许一个进程在管程内执行某个内部过程。管程中设置条件变量和等待/唤醒操作,以解决同步问题。
并发编程领域的两大问题–互斥、同步,都可以通过管程来解决。管程的解决方法如下:
互斥:管程将共享变量及对共享变量的操作统一封装起来,并通过一个互斥锁来保证同一时刻只允许一个进程进入管程,以实现对共享变量的互斥访问。这个互斥锁一般是由编译器自动添加的。
同步(时序):管程中引入了条件变量,而且每个条件变量都对应有一个等待队列,通过这两者来提供同步的机制。条件变量有wait()、signal()两种操作原语,管程中的过程通过调用条件变量的操作原语来封装了进程的同步操作,使进程在条件不满足时被阻塞,条件满足时被唤醒。进程调用管程中的过程就可以实现同步。但管程的过程里何时应该阻塞进程,何时唤醒进程都是由程序员自己决定的,管程模型并不知道你的程序逻辑。
管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。但是管程更容易使用,所以 Java 选择了管程。
管程就像程序封装的思想,把进程同步互斥这些复杂的细节放进管程定义的函数之内,而我们调用只需要函数接口即可。