本文主要内容:
- 管程(Monitor)介绍
- 管程实现
- 管程应用
一、管程(Monitor)介绍
1.1 管程
前一篇文章介绍了信号量以及使用,信号量已经提供了一个方便且高效的进程同步机制,但是信号量有个缺点就是每次都需要程序员专门的去调用PV操作,如果程序员由于大意调用错了PV操作,比如该调用P操作的时候却调用了V操作,该针对X信号量调用P操作,却对Y信号量调用了P操作。这种错误是非常危险的,因为进程同步的问题不是每次都能重现,比如前面的错误在测试环境可能不会出现错误,但是到了生产环境就有可能出现,且无法重现。
为了解决上述问题,引入了管程(Monitor),信号量是操作系统层面的结构,管程是一个程序结构,由编程语言封装,最终由编译器:
- 一个管程定义了一个数据结构和能够并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据
- 进程可以调用管程的操作,但是不能访问管程的内部数据结构
- 它表示一个对象自己维护自己的状态,并且能够根据自身状态来同步并发的线程操作,而不是把这种同步的手段交给调用者来处理。
- 管程保证同一时间只有一个进程处在管程内部的方法内
monitor MonitorName
{
//shared variable declarations
procedure P1(...)
{
}
procedure P2(...)
{
}
procedure P3(...)
{
}
procedure PN(...)
{
}
initialization code(...)
{
}
}
1.2 条件(Condition variables)
只有管程是不够的,在生产者消费者的例子中,当发现buffer中如果已经没有item的时候,消费者如何block自己呢?介于这个原因,又引入了条件变量(Condition Variables)。条件变量同时提供了两个方法:P以及V用于block和unblock。
二、管程实现
管程通过队列来跟踪那些尝试着想访问管程的进程,为了排他访问,管程必须得有一个锁,访问管程的进程会得到这个锁,其他尝试访问管程的进程就得block。被block的进程会被放入队列,等待被unblock。
管程的实现中有如下的队列:
- 进入队列(Entry Queue),保存尝试从外部访问管程程序的进程,每个管程至少有一个进入队列
- 被唤醒的队列(Signaller Queue),保存刚刚执行了V操场的进程(signal)
- 等待队列(Waiting Queue),保存刚刚被V操作唤醒的进程
- 条件变量队列(Condition Variable Queue),保存刚刚执行了条件变量Wait(P)操作的进程
三、管程应用
上一篇文章针对哲学家用餐给出的解决方案有个问题:当所有哲学家同时拿起筷子想吃饭的时候就会发生死锁,所有哲学家都会一直处于等待状态。本文用管程给出了解决方案,哲学家吃饭得满足的条件:
- 哲学家饿(hungry)
- 哲学家的左右邻居都没有吃饭
如果满足上面条件,则哲学家便可以拿起筷子进行用餐,否则哲学家就得等待
monitor dp
{
enum {thinking, hungry, eating} state[5];
condition self[5];
void pickup(int i) {
state[i] = hungry;
test(i);
if (state[i] != eating)
self[i].wait();//P操作
}
void putdown(int i) {
state[i] = thinking;
test( (i+4)%5 );
test( (i+1)%5 );
}
void test(int i) {
if ((state[(i+4)%5] != eating) &&
(state[i] == hungry) &&
(state[(i+1)%5] != eating)) {
state[i] = eating;
self[i].signal();//V操作
}
}
void init() {
for (int i = 0; i < 5; i++)
state[i] = thinking;
}
}
Condition Self代表五个哲学家,当不能吃时候进行wait。
db.pickup(i);//找筷子
...
eating//吃
...
dp.putdown(i);//放下筷子
管程更多的是对资源的并发访问做了一次封装,感觉有很多OO编程的思想,直接用信号量的话,各种控制会很发散,当然容易出错,信号量本事是没有问题的。但是使用容易出错。且由于散发在多出,出问题不好排查,也不好修复。
管程是一种编程思想,这种思想就是封装,并且有DRY的感觉。