并发性:并发执行的各个进程之间,既有独立性,又有制约性;
独立性:各进程可独立地向前推进;
制约性:一个进程会受到其他进程的影响,这种影响关系可能有3种形式:
现在有P1和P2两个进程共享一个变量count:
由于两个进程是异步的,所以它们执行的顺序不确定,这就会造成运行结果不可再现,除非规定它们使用共享变量的先后顺序。
进程的同步:系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成一项任务,即:
两个进程可以类比为接力赛中一前一后的两个队友:
进程的互斥:
互斥——不能“同时”的操作:对于系统一些共享资源,只有被释放后,才可以重新被操作;
进程互斥——进程间竞争使用不能被“同时”操作的共享资源的关系。
1. 临界资源
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。
2. 临界区
在进程中涉及到临界资源的程序段叫做临界区。多个进程指针对统一资源的临界区称为相关临界区。
3. 实现各进程互斥进入临界区(互斥使用临界资源)
进程需要在临界区前加上一段用于申请资源的代码,称为进入区;在临界区后加上一段用于释放资源的代码,称为退出区。
while ( 1 )
{
进入区代码(使用前申请);
临界区代码(使用临界资源);
退出区代码(释放资源);
其余代码;
}
4. 进入临界区(使用临界资源)的四项原则:
5. 进程互斥的解决主要有两种,分别是硬件方法和软件方法。
1. 中断禁用:为保证互斥,只需保证进程不被中断就可以了,通过系统中有关中断的原语即可实现。其实就是把中断这个操作当作临界资源来看。
2. 专用机器指令:就是设置一个bolt值,通过它来看是否可以使用临界资源:
信号量是一个记录型的数据结构;
定义如下:
struct semaphore {
int value; //信号量的值
pointer_PCB queue;
}
semaphore s; // 定义s为信号量
除初始化外,仅能通过两个标准的原子操作 wait(s) 和 signal(s) 来访问信号量。这两个操作一直被称为P、V操作。
原子操作(原语):在执行上不可被中断的操作。
P(s) { // =wait(s)
s.value--;
if (s.value < 0)
block(s.queue);
// 将调用该P操作的进程放入与s有关的阻塞队列
}
V(s) { // = signal(s)
s.value++;
if (s.value <= 0)
wakeup(s.queue);
// 从有关s的阻塞队列唤醒一个进程放入就绪队列
}
信号量 s 的物理含义:
P、V操作的含义:
信号量的初值应 >= 0
必须设置一次且只能设置一次初值;
初值不能为负数;
只能通过 wait(s) 和 signal(s) 来操作信号量。
设P1和P2是两个进程,它们都需要使用打印机进行打印,这时可以定义一个信号量mutex,其初值为1,用于实现P1、P2进程对打印机的互斥访问:
semaphore mutex = 1; // 表示现有的打印机资源数量为1
//P1
while (true) {
P(mutex);
use printer;
V(mutex);
}
//P2
while (true) {
P(mutex);
use printer;
V(mutex);
}
现有两个进程P1和P2,其中P1需要先执行,才能执行P2,遵循两者的执行顺序,我们可以画出进程的前驱图:
这其中,s是我们设置的信号量,其初值为0,所以在执行中,需要先进行V操作(++)再进行P操作(--),由此可以实现进程同步:
semaphore s = 0;
//P1
{
P1 code;
V(s);
}
//P2
{
P2 code;
P(s);
}
如此可以实现先执行P1,再执行P2。
1. 单缓冲区:生产者进程P和消费者进程C共用一个缓冲区,P生产产品放入缓冲区,C从缓冲区取产品来消费。
单缓冲区的同步问题:
单缓冲区互斥问题:
semaphore empty = 1; // 表示缓冲区空位数
semaphore full = 0; //表示缓冲区产品数
//P进程
while (true) {
produce a product;
P(empty);
put product in buffer;
V(full);
}
//C进程
while (true) {
P(full);
get product from buffer;
V(empty);
consume the product;
}
semaphore empty = n; // 表示缓冲区有n个空位
semaphore full = 0;
//P进程
while (true) {
produce a product;
P(empty);
put product in buffer[in];
in = (in + 1) mod n; //将产品放入缓冲区中后,in指针向后移动一位,这里的缓冲区相当于一个栈
V(full);
}
//V进程
while (true) {
P(full);
get product from buffer[out];
out = (out + 1) mod n;
V(empty);
consume the product;
}
因为多个生产者和多个消费者都要使用到缓冲区,所以缓冲区就是我们的临界资源,为了实现生产者之间以及消费者之间的互斥关系,需要引进信号量mutex,并且其C、V操作需要放在临界区前后:
semaphore empty = n;
semaphore full = 0;
semaphore mutex = 1; // 实现同类进程间对缓冲区的互斥访问
//P进程
while (true) {
produce a product;
P(empty);
P(mutex);
put product in buffer[in];
in = (in + 1) mod n;
V(mutex);
V(full);
}
//V进程
while (true) {
P(full);
P(mutex);
get product from buffer[out];
out = (out + 1) mod n;
V(mutex);
V(empty);
consume the product;
}
同步:当缓冲区已放满了产品时,生产者进程必须等待;当缓冲区已空时,消费者进程应该等待;
互斥:所有进程应互斥使用缓冲区资源;
实际上,在多缓冲情况下,为提高系统并发性,只是同类进程应当互斥!
在多缓冲区问题中,如果把进程P或C的mutex和另一个相邻的变量互换,就会发生死锁,死锁就是在一个进程进展到某一步时被其他进程抢占后进入阻塞状态,其他进程由于某一变量未达到条件也进入了阻塞状态。所以记住要把mutex放里面,不包含其他的变量。
设置两个不相干的mutex变量,这样就只是同类进程间互斥,提高了系统并发性。
有两组并发进程:读者和写者,共享一组数据区,为保证数据的一致性和完整性,规定如下:
若读者优先,即当写者提出了写的要求后,允许新的读者进入。则代码如下:
wrt = 1; // 表示能否写,初始可写
mutex = 1; // 实现不同读者进程的互斥访问
readcount = 1; // 表示读进程数
//读者进程
while (true) {
P(mutex);
readcount++;
if(readcount == 1)
P(wrt);
V(mutex);
read;
P(mutex);
readcount--;
if(readcount == 0)
V(wrt);
V(mutex);
}
// 写者进程
while (true) {
P(wrt);
write;
V(wrt);
}
P、V操作必须成对出现,有一个P操作就一定有一个V操作;
当信号量用于实现进程互斥时,对于同一信号量的P、V操作处于同一进程;
当信号量用于实现进程同步时,对于同一信号量的P、V操作处于不同进程;
如果P(S1)和P(S2)两个操作在一起,那么P操作的顺序至关重要,一个同步P操作与一个互斥P操作在一起时,同步P操作在前,互斥P操作在后,而两个V操作则无关紧要。