我们将一次仅允许一个进程访问的资源称为临界资源
,而临界区
是指访问临界资源
的那段代码。
通常将互斥信号量设置为 mutex
,初始值为 1
。
为什么初始值设置为 1 呢?
因为数值表示访问临界资源的进程数量,作为进程互斥,同时就只能有一个进程访问临界资源,所以是 1 。
对于不同的临界资源,需要设置不同的互斥信号量。
这里来看一段代码:
semaphore mutex = 1; // 初始化信号量
P1(){
...
P(mutex); //使用临界资源前需要加锁
临界区代码段...
V(mutex); //使用临界资源后需要解锁
...
}
P2(){
...
P(mutex);
临界区代码段...
V(mutex);
...
}
按照代码的顺序模拟一下互斥操作:
P(mutex)
会将 mutex
的值减一,此时 mutex=0
;mutex=-1
,那么 P2 进程就会执行 block
原语把自己阻塞起来;mutex
值加一,然后唤醒 P2 进程。P 操作是对资源的申请,V 操作是对资源的释放,PV 操作必须成对出现。
进程同步的目的就是要让并发进程按照要求有序地推进。
P1(){
代码1;
代码2;
代码3;
}
P2(){
代码4;
代码5;
代码6;
}
上面的代码由于异步性导致执行顺序是不可预知的。
但是有时候我们必须要保证代码的执行顺序,假设 代码4 是基于 代码3 的执行结果来的,所以3必须在4前面执行。
我们可以设置一个同步信号量 S=0
;
然后在前一个操作之后执行 V 操作,在后一个操作之前执行 P 操作。
semaphore S = 0; //初始化信号量为0
P1(){
代码1;
代码2;
代码3;
V(S);
}
P2(){
P(S);
代码4;
代码5;
代码6;
}
如果先执行 P2,他会执行 block
原语阻塞自己,从而得以先执行 P1 保证执行顺序。
前驱图如下所示:
即有 6 个代码,需要按照图中的顺序执行。
解决这个问题可以分 3 步:
所以结果如下:
semaphore a = 0; //初始化信号量为0
semaphore b = 0;
semaphore c = 0;
semaphore d = 0;
semaphore e = 0;
semaphore f = 0;
semaphore g = 0;
P1(){
S1;
V(a);
V(b);
}
P2(){
P(a);
S2;
V(c);
V(d);
}
P3(){
P(b);
S3;
V(g);
}
P4(){
P(c);
S4;
V(e);
}
P5(){
P(d);
S5;
V(f);
}
P6(){
P(e);
P(f);
P(g);
S6;
}