上一个章节中,我们讲了什么是信号量,如何用信号量来实现进程之间的同步、互斥。
但是吧,用信号量来实现的话,好麻烦哟,在各个进程之间都要大量的 PV 操作,而且操作不当一不小心就死锁
了,为了锻炼大家如何更好的使用 PV 操作,其实在进程同步互斥,有几个比较经典的案例,比如说:生产者、消费者问题、多生产者、多消费者问题、吸烟者、读者写者、哲学家进餐等经典用来锻炼进程同步逻辑的问题,当然感兴趣的同学可以自行去学习和了解,我这里就不都说了。
信号量的缺点也比较显而易见,PV操作在各个进程中都需要去实现,不便于管理,容易发生死锁。在 1974 年和 1977 年, Hore 和 Hansen 提出了管程。
什么是管程
说白了,管程也是用来解决进程同步互斥的一种工具,他的操作比信号量更加简单,管程封装了同步操作,对进程隐藏了同步细节。
那管程有什么好处?
1、把分散在各个进程中临界区集中起来进行管理;
2、防止进程有意或者无意的违法同步操作;
3、便于用高级语言来写程序;
说了这些好处,第一次看没理解也是很正常的,只能把管程了解透彻,才能明白它的好处,我们接着往下看。
管程的组成
管程,你可以把它当作一个类看作,一个类里面有啥? 在 Java 类里面可以有属性、构造方法、业务方法,管程它里面也有这些东西。
1、局部于管程的共享变量,这里就可以理解类里面的属性。
2、对数据结构进行操作的一组过程,这里可以理解成业务方法,在方法里面我们可以操作类属性。
3、对局部于管程的数据进行初始化的语句,可以理解为构造方法,初始化属性值。
而且管程还有这么几个特性:
1、管程内的数据,只能被管程里面的方法所访问,管程内部的共享变量对外是不能直接访问的。
2、一个进程只能调用管程中的方法,才能访问管程内的共享数据。
3、每一次,只允许一个进程调用管程内的某个方法。
管程实现互斥、同步
首先我们来用伪代码实现一个管程:
monitor ProducerConsumer
// 定义条件变量来实现同步
condition full,empty;
int count = 0; // 缓冲区中的产品数
// 定义插入的方法
void insert(Item item){
if (count == N){
wait(full)
}
count++;
insert_item(item);
if(couont == 1){
signal(empyt)
}
}
// 定义移除的方法
Item remove(){
if(count == 0){
wait(empty);
}
count--;
if(count == N-1){
signal(full);
}
return
}
end monitor
在这里我们定义了一个生产者和消费者的一个管程,里面有两个函数,一个是 insert、一个是 remove,每次 insert 只能有一个进程进行操作,也就是互斥。 消费者必须要等到生产者执行完成之后,消费者才有东西可以消费,这里就是同步。
我们先看互斥,假设现在生产者/消费者的代码如下:
product(){
item = 有一个新的产品;
ProducerConsumer.insert(item)
}
consumer(){
item = ProducerConsumer.remove()
}
在这里,消费者是直接调用了 insert 的函数,如果在这个有多个线程并发的来执行调用 insert 函数这句代码,只会有一个进程能进入,这个是由编译器负责实现各个进程互斥进入管程的过程
。
而消费数据,管程里面则是利用了 count 属性,以及 P(wait) V(signal) 操作,来控制,必须要生产者先执行 V 操作,消费者才能被唤醒。
其实大家也能看到,管程之所以被说是一个类,是因为管程采用了封装的思想,把复杂的细节隐藏了,我们只需要调用管程提供的特定“入口”就能实现进程同步/互斥了