进程同步(synchronization):指系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成一项任务。形象的说。一个进程运行到某一点时,要求另一伙伴进程为它提供消息,在未获得消息之前,该进程进入阻塞态,获得消息后被唤醒进入就绪态。
进程同步机制
一、信号量及PV操作
信号量是一个特殊变量,用于进程间传递信息,通常是一个整数值,由Dijkstra提出,最初用于解决互斥问题,信号量只有两个(0,1),称作二元信号量;
后来推广到多值,用于解决同步问题。注意信号量是一个静态的值,多个进程可以共享这个内存单元
这里我们举例定义一个信号量的结构体(也可以定义为一个单一整型)定义如下:
struct semaphore{
int count;
queueType queue;
}s;
其中count用于传递信息,进程可以挂到队列queue上对信号量可以实施的操作只有三个:1、初始化;2、P操作;3、V操作(P、V分别是荷兰语proberen(test)和verhogen(increment))
P操作:
P(s){
s.count--;
if(s.count < 0){
//将调用P操作的进程状态置为阻塞态,并插入相应等待队列s.queue末尾;
//重新调度
}
}
V操作:
V(s){
s.count++;
if(s.count <= 0){
//唤醒s.queue中等待的一个进程,改变其状态为就绪态,并将其插入就绪队列
//并不会立即切换到被唤醒的进程,而是调用V操作的进程继续执行
}
}
用PV操作解决进程间互斥问题
对同一个信号量的PV操作在相同的代码块,即可解决互斥问题
- 划定临界区
- 设置信号量count初值为1
- 在临界区前实施P(s)
- 在临界区之后实施V(s)
s.count = 1;
P(s.count);
//临界区
V(s.count)
用PV操作解决同步问题(生产者消费者问题)
如果对同一个信号量的PV操作分部在不同的代码块中,就很巧妙的解决了同步问题
|
|
其中empty是空缓冲区的个数,也就是可以生产多少item,初值为N;
full表示满缓冲区的个数,即可消费的item有多少,初值为0;
mutex初值为1,用于解决互斥问题;
可以看到,对empty和full的操作分布在不同的函数中,在producer中,如果空缓冲区个数满了,则进入阻塞态,等待消费者消费,同理consumer,一开始full为0,则阻塞等待生产者生产,当生产者生产item之后会进行V(&full)操作唤醒消费者被阻塞的进程
思考:
1、这两个函数中的两个P操作是否可以颠倒
如果消费者进程先执行,会将mutex上锁,且自身由于无item可消费,进入阻塞态;而生产者由于mutex互斥,无法将item放入缓冲区,这样就造成了死锁
2、两个V操作是否可以颠倒
由于V操作不会使得调用V操作的进程进入阻塞态,因此是可以的,但是互斥在设计的原则应将临界区范围缩到最小,这样不会影响生产和消费的效率
二、管程
由于PV操作在界定临界区及解决互斥问题容易出错,编写程序较困难,因此出现了管程这种更完善的同步机制
管程是由共享资源数据结构及在其上操作的一组过程(方法、函数)组成
进程和管程的关系:
进程只能通过调用管程中的过程来间接访问管程中的数据结构
管程是互斥进入的,其互斥性是由编译器负责保证;
同步的解决方案是在管程中设置条件变量及wait/signal操作。这里需要注意,如果一个进程或线程在条件变量上等待,应该先释放管程的使用权,使其他线程或进程可以操作管程
管程的种类
当一个线程进入管程并执行唤醒操作(A唤醒B),这时管程中就存在两个同时处于活动状态的进程,A和B运行的先后顺序不同,导致管程的种类不同
- A等待,B执行,例如Hoare管程
- A继续执行,B等待,典型有MESA管程
- 唤醒操作为管程中最后一个可执行操作,意思就是A在唤醒B之后立刻出管程,B在管程内继续执行,采用这种策略的有Hansen管程
Hoare管程
- 管程入口处设置一个进程等待队列,因为管程是互斥进入的,因此设置一个入口等待队列让试图进入已被占用的管程的进程等在队列中
- 如果一个进程不满足条件,则进入阻塞态,等在条件变量上,每个条件变量都有独立的队列
- 如果进程A唤醒进程B,则A等待,B执行,而A等待在紧急等待队列中,紧急等待队列的优先级高于入口等待队列
条件变量是在管程内部使用的一种特殊类型的变量(静态),假设定义为c,条件变量的等待队列为cqueue
对于c,可以执行wait(c)和signal(c)操作
wait(c):如果紧急等待队列为非空,唤醒第一个等待者;否则释放管程互斥权,入口等待队列队首的进程进入管程,执行此操作的进程进入cqueue的队尾
signal(c):如果cqueue为空,则相当于空操作,执行此操作的进程继续执行;否则唤醒第一个等待者,执行此操作的进程进入紧急等待队列的末尾
Hoare管程的缺点
由于Hoare管程在A唤醒B后立刻执行B,此时A的时间片并没有用完,导致增加了许多上下文切换的操作,为了避免这种弊端,MESA管程应运而出
MESA管程
MESA管程的没有signal操作,而被称作notify,更形象
notify操作通知一个等在cqueue上的进程被转移到紧急等待队列中,假如A进程执行notify(c),使得B进程进入紧急等待队列,当A被切换下cpu时,不能保证立刻将B切换执行,中间有可能有多个进程影响条件变量c,因此在B进程执行前还需要检查条件变量是否成立
优化:
- 可以在执行notify操作时,关联一个动作,这个动作将条件变量上等待了一定时间的进程无论是否被notify都切换为就绪态,避免个别进程由于等待无限期推迟而处于饥饿状态
- notify改进为notifyAll
管程的应用
我们用管程解决生产者和消费者问题(这里只给出了生产insert的操作,采用Hoare管程)
class Monitor{
static final int N = 100;
private int buffer[] = new int[N];
private int count = 0, hi = 0;
/**
* 用synchronized解决互斥
*/
public synchronized void insert(int item) {
if(count == N)
go_to_sleep();
buffer[hi] = item;
hi = (hi+1) % N;//update hi
count += 1;
if(count == 1)
notify();
}
}
如果采用MESA管程,要将
if(count == N)改为 while(count == N)
因为MESA管程需要不断检查条件变量,并不是直接可以执行