并发性:并发执行的各个进程之间,既有独立性,又有制约性;
独立性:各进程可独立地向前推进;
制约性:一个进程会受到其他进程的影响,这种影响关系可能有3种形式:
- 互斥:一种竞争关系
- 同步:一种协作关系
- 通信:交换信息
5.1 并发的原理
5.1.1 与时间有关的错误
现在有P1和P2两个进程共享一个变量count:
由于两个进程是异步的,所以它们执行的顺序不确定,这就会造成运行结果不可再现,除非规定它们使用共享变量的先后顺序。
5.1.2 互斥与同步的概念
进程的同步:系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成一项任务,即:
- 一个进程运行到某一点时要求另一伙伴进程为它提供消息;
- 未获得消息之前,该进程处于等待状态;
- 获得消息后被唤醒进入就绪状态。
两个进程可以类比为接力赛中一前一后的两个队友:
进程的互斥:
互斥——不能“同时”的操作:对于系统一些共享资源,只有被释放后,才可以重新被操作;
进程互斥——进程间竞争使用不能被“同时”操作的共享资源的关系。
5.1.3 临界区与进程互斥
1. 临界资源
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。
2. 临界区
在进程中涉及到临界资源的程序段叫做临界区。多个进程指针对统一资源的临界区称为相关临界区。
3. 实现各进程互斥进入临界区(互斥使用临界资源)
进程需要在临界区前加上一段用于申请资源的代码,称为进入区;在临界区后加上一段用于释放资源的代码,称为退出区。
while ( 1 )
{
进入区代码(使用前申请);
临界区代码(使用临界资源);
退出区代码(释放资源);
其余代码;
}
4. 进入临界区(使用临界资源)的四项原则:
- 空闲让进:当无进程在互斥区时,任何有权使用互斥区的进程可进入;
- 忙则等待:不允许两个以上的进程同时进入互斥区;
- 有限等待:任何人进入互斥区的要求应在有限的时间内得到满足;
- 让权等待:处于等待状态的进程应放弃占用CPU,以使其他进程有机会得到CPU的使用权。
5. 进程互斥的解决主要有两种,分别是硬件方法和软件方法。
5.1.4 硬件支持互斥的方法
1. 中断禁用:为保证互斥,只需保证进程不被中断就可以了,通过系统中有关中断的原语即可实现。其实就是把中断这个操作当作临界资源来看。
2. 专用机器指令:就是设置一个bolt值,通过它来看是否可以使用临界资源:
5.2 信号量机制
5.2.1 信号量的概念
信号量定义
信号量是一个记录型的数据结构;
定义如下:
struct semaphore {
int value; //信号量的值
pointer_PCB queue;
}
semaphore s; // 定义s为信号量
P、V操作
除初始化外,仅能通过两个标准的原子操作 wait(s) 和 signal(s) 来访问信号量。这两个操作一直被称为P、V操作。
原子操作(原语):在执行上不可被中断的操作。
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的阻塞队列唤醒一个进程放入就绪队列
}
P、V操作的含义
信号量 s 的物理含义:
- s > 0 表示有 s 个资源可用;
- s = 0 表示无资源可用;
- s < 0 则 s 绝对值就是等待队列中的进程个数
P、V操作的含义:
- P(s) :表示申请一个资源
- V(s) :表示释放一个资源
信号量的初值应 >= 0
信号量的使用
必须设置一次且只能设置一次初值;
初值不能为负数;
只能通过 wait(s) 和 signal(s) 来操作信号量。
如何用信号量实现进程间互斥问题?
- 找出临界资源,设置信号量。有几类资源就设置几个信号量,对每类资源,资源数量就是对应的信号量初值;
- 划分处临界区(涉及到临界资源的代码,不要人为添加条件);
- 临界区前加上对应信号量的P操作,临界区后加上对应信号量的V操作。
5.2.2 信号量的应用
利用信号量实现进程互斥
设P1和P2是两个进程,它们都需要使用打印机进行打印,这时可以定义一个信号量mutex,其初值为1,用于实现P1、P2进程对打印机的互斥访问:
semaphore mutex = 1; // 表示现有的打印机资源数量为1
//P1
while (true) {
P(mutex); // 用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
{
P(s);
P2 code;
}
如此可以实现先执行P1,再执行P2。
5.2.3 生产者 - 消费者问题
1. 单缓冲区:生产者进程P和消费者进程C共用一个缓冲区,P生产产品放入缓冲区,C从缓冲区取产品来消费。
单缓冲区面临的问题
单缓冲区的同步问题:
- 当缓冲区存在空位的时候,P进程才能往里面放产品,设置信号量为empty,初值为1,表明缓冲区存在空位;
- 当缓冲区有产品的时候,C进程才能从里面取产品,设置信号量为full,初值为0,因为缓冲区刚开始是没有产品的;
单缓冲区互斥问题:
- P、C进程不能同时使用缓冲区
1. 单缓冲区的单生产者 - 单消费者
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;
}
2. 多缓冲区的单生产者 - 单消费者
semaphore empty = n; // 表示缓冲区有n个空位
semaphore full = 0; // 表示缓冲区有0个产品
int in = 0; // 表示将产品放入缓冲区的指针
int out = 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;
}
3. 多缓冲区的多生产者 - 多消费者
因为多个生产者和多个消费者都要使用到缓冲区,所以缓冲区就是我们的临界资源,为了实现生产者之间以及消费者之间的互斥关系,需要引进信号量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和另一个相邻的变量互换,就会发生死锁,死锁就是在一个进程进展到某一步时被其他进程抢占后进入阻塞状态,其他进程由于某一变量未达到条件也进入了阻塞状态。
在多缓冲区的多生产者 - 多消费者问题里,如果把P(mutex)放在前面,如果消费者进程先运行,那么消费者先占用了mutex,也就是缓冲区,但是在申请full时由于初值为0而进入阻塞队列,此时轮到生产者进程,生产者由于mutex此时为0也进入阻塞队列,双方相互制约,导致任务会永远阻塞下去,这就是死锁。
所以记住要把P操作的mutex放里面,不包含其他的变量。
多缓冲区的生产者 - 消费者问题解法2
设置两个不相干的mutex变量,这样就只是同类进程间互斥,提高了系统并发性。
如何用信号量解决进程间同步问题?
- 找出进程间的前驱关系;
- 针对每个前驱关系设置信号量,通常初值为0,具体情况要根据题目来分析;
- 前驱进程的后面加上对应信号量的V操作,后继进程的前面加上对应信号量的P操作。
5.2.4 哲学家进餐问题
如果5人同时拿起左边筷子,再想要拿起右边的筷子时,就会发生死锁!
为了防止死锁,我们可以规定,仅当一个哲学家左右两边的筷子都可以用的时候,才允许他拿筷子。
#define N 5
#define THINKING 0
#define HUNGRY 1
#define EATING 2
int state[N]; // 每个哲学家的状态,初值都为0
semaphore mutex = 1; //互斥访问哲学家的状态
semaphore s[N]; // 哲学家是否可以吃,初值都为0,表示不能吃
void test(int i) {
if (state[i] == HUNGRY && state[(i - 1) % 5] != EATING && state[(i + 1) % 5] != EATING) {
state[i] = EATING;
V(s[i]);
}
}
void puilosopher(int i) {
while (true) {
thinking;
P(mutex);
state[i] = HUNGRY;
test[i];
V(mutex);
P(s[i]);
take left fork;
take right fork;
eating;
put left fork;
put right fork;
P(mutex);
state[i] = THINKING;
test([i - 1] % 5);
test([i + 1] % 5);
V(mutex);
}
}
5.2.5 读者 - 写者问题
有两组并发进程:读者和写者,共享一组数据区,为保证数据的一致性和完整性,规定如下:
- 允许多个读者同时执行读操作
- 不允许读者、写者同时操作
- 不允许多个写者同时操作
读者优先
若读者优先,即当写者提出了写的要求后,允许新的读者进入。则代码如下:
wrt = 1; // 代表一个共享文档,实现不同写者对共享文档的互斥访问,同时实现读者和写者对共享文档的互斥访问
readcount = 1; // 表示读进程数
mutex = 1; // 实现不同读者对readcount的互斥访问
//读者进程
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);
}
写者优先
若写者优先,就表明当写者提出了新的写的要求后,就不允许有新的读者进入了,代码如下:
int readcount = 0; // 读进程数
int writecount = 0; // 写进程数
semaphore mutex = 1; // 实现读者和写者对共享文档的互斥访问
semaphore rc_mutex = 1; // 实现写者间对writecount的互斥访问
semaphore w = 1;
semaphore wc_mutex = 1; // 实现读者间对readcount的互斥访问
// 读者进程
while (true) {
P(w);
P(rc_mutex);
readcount++;
if (readcount == 1)
P(mutex);
V(rc_mutex);
V(w);
read;
P(rc_mutex);
readcount--;
if (readcount == 0)
V(mutex);
V(rc_mutex);
}
// 写者进程
while (true) {
P(wc_mutex);
writecount++;
if (writecount == 1)
P(w);
V(wc_mutex);
P(mutex);
write;
V(mutex);
P(wc_mutex);
writecount--;
if (writecount == 0)
V(w);
V(wc_mutex);
}
读写公平
基于读者优先的代码,在写者进程、读者进程的加入读者部分前后多加一个mutex锁来实现读写公平(不让读者插队)。
信号量应用小结
P、V操作必须成对出现,有一个P操作就一定有一个V操作;
当信号量用于实现进程互斥时,对于同一信号量的P、V操作处于同一进程;
当信号量用于实现进程同步时,对于同一信号量的P、V操作处于不同进程;
如果P(S1)和P(S2)两个操作在一起,那么P操作的顺序至关重要,一个同步P操作与一个互斥P操作在一起时,同步P操作在前,互斥P操作在后,而两个V操作则无关紧要。
练习1:农夫 - 猎人 问题
题目
一个笼子,可以放猴子或鸭子。猎人向笼子放猴子,农夫向笼子放鸭子;动物园从笼子取猴子,餐馆从笼子取鸭子。笼中一次只能放一个动物,请用P,V操作实现。
解答
semaphore empty = 1; // 笼子空间
semaphore monkey = 0; // 笼中猴子
semaphore duck = 0; // 笼中鸭子
void farmer {
P(empty);
put a duck into cage;
V(duck);
}
void hunter {
P(empty);
put a monkey into cage;
V(monkey);
}
void zoo {
P(monkey);
get a moonkey from cage;
V(empty);
}
void restaurant {
P(duck);
get a duck from cage;
V(empty);
}
练习2:父母 - 子女 问题
题目
有父母子女四人围坐一起吃水果,父亲不断削苹果往盆中放,母亲不断削梨往盆中放,女儿则从盆中取苹果吃,儿子则从盆中取梨吃。
1)假设盆足够大;
2)假如盆中最多只能放N只水果;
试用P、V操作协调他们的关系。
解答
#define APPLE 1
#define PEAR 2
int in = 0;
int out = 0;
semaphore basin = n;
semaphore apple = 0;
semaphore pear = 0;
void father {
product an apple;
P(basin);
basin[in] = APPLE;
in = (in + 1) % n;
V(apple);
}
void mother {
product a pear;
P(basin);
basin[in] = PEAR;
in = (in + 1) % n;
V(pear);
}
void daughter {
P(apple);
while (basin[out] != APPLE)
out = (out + 1) % n;
eat an apple;
basin[out] = 0;
out = (out + 1) % n;
V(basin);
}
void son {
P(pear);
while (basin[out] != PEAR)
out = (out + 1) & n;
eat a pear;
basin[out] = 0;
out = (out + 1) % n;
V(basin);
}
练习3:三个进程 - 两个缓冲区 问题
题目
三个进程A、B、C,共享两个缓冲区B1、B2。缓冲区B1可放n件产品,缓冲区B2可放m件产品。进程A每次生产一件产品并将其放到缓冲区B1中;进程B每次从缓冲区B1中取一件产品后再把它放到缓冲区B2中,进程C每次从缓冲区B2中取一件产品消费。为了防止把产品存入满的缓冲区,或从空的缓冲区取产品,试用P、V操作实现它们之间的相互制约。
解答
int B1[n], B2[m]; // 表示两个缓冲区
int in1 = 0, out1 = 0;
int in2 = 0, out2 = 0;
semaphore empty1 = n, empty2 = m; // 表示缓冲区的空位数量
semaphore full1 = 0, full2 = 0; // 表示缓冲区的产品数量
void A() {
while (true) {
produce a product;
P(empty1);
put a product in B1[in1];
in1 = (in1 + 1) % n;
V(full1);
}
}
void B() {
while (true) {
P(full1);
get a product from B1[out1];
out1 = (out1 + 1) % n;
V(empty1);
P(empty2);
put a product in B2[in2];
in2 = (in2 + 1) % m;
V(full2);
}
}
void C() {
while (true) {
P(full2);
get a product from B2[out2];
out2 = (out2 + 1) % m;
V(empty2);
}
}
练习4:博物馆 问题
题目
某博物馆最多容纳500人同时参观,有一个出入口,该出入口一次仅允许一个人通过。请写出利用PV操作实现上述过程中的同步与互斥完整过程,并说明信号量的含义和初值。
解答
semaphore museum = 500; // 表示博物馆当前容量
semaphore people = 0; // 表示博物馆中的人数
semaphore door = 1; // 实现visitor之间对door的互斥访问
void visitor() {
P(museum);
P(door);
get into museum;
V(door);
V(people);
P(people);
P(door);
get out of museum;
V(door);
V(museum);
}
练习5:发消息 问题
题目
有四个进程A、B、C、D,进程A通过一个缓冲区不断地向B、C、D发送消息,A每向缓冲区写入一个消息后,必须等进程B、C、D都读取后才可以发送下一个消息,B、C、D对A写入的每个消息各读取一次。试用P、V操作实现他们的正确通讯。
解答
semaphore tob = toc = tod = 0; // A发送的消息
semaphore b = c = d = 1; // B、C、D可接受的消息数
void A() {
while (true) {
P(b), P(c),P(d);
send a message;
V(tob), V(toc), V(tod);
}
}
void B() {
while (true) {
P(tob);
get a message;
V(b);
}
}
void C() {
while (true) {
P(toc);
get a message;
V(c);
}
}
void D() {
while (true) {
P(tod);
get a message;
V(d);
}
}
练习6:过桥问题
题目
某条河上只有一座独木桥,以便行人过桥。现在河的两边都有人要过桥,过桥规则:同一方向的可以连续过桥;一方有人过桥另一方的人要等待。为了,请用P、V操作保证他们安全过桥。
解答
int leftcount = 0; //表示左边要过桥的人
int rightcount = 0; //表示右边要过桥的人
semaphore bridge = 1; // 表示桥的是否可以通过
semaphore mutex1 = 1, mutex2 = 1; // 分别实现左右两边人对各自count的互斥访问
void left() {
P(mutex1);
leftcount++;
if (leftcount == 1)
P(bridge);
P(mutex1);
across the bridge;
P(mutex1);
leftcount--;
if (leftcount == 0)
V(bridge);
V(mutex1);
}
void right() {
P(mutex2);
rightcount++;
if (rightcount == 1)
P(bridge);
P(mutex2);
across the bridge;
P(mutex2);
rightcount--;
if (rightcount == 0)
V(bridge);
V(mutex2);
}
练习7:仓库 问题
题目
有一仓库可以存放A,B两种物品,每次只能存入一物品(A或B)存储空间无限大,只是要求:
-n < count(A) – count(B) <m 其中n,m是正整数;
进程A负责存入A物品,进程B负责存入B物品,用P,V操作实现进程的有序推进。
解答
semaphore a = m - 1; // 当仓库只放A时,A最大可以放m-1个
semaphore b = n - 1; // 当仓库只放B时,B最大可以放n-1个
semaphore mutex = 1; // 实现存放A和B的进程间对仓库的互斥访问
void A() {
while (true) {
P(a);
P(mutex);
put A in storage;
V(mutex);
V(b); // 当仓库多放了一个A时,就可以多放一个B
}
}
void B() {
while (true) {
P(b);
P(mutex);
put B in storage;
V(mutex);
V(a);
}
}
练习8:司机 - 售票员 问题
题目
在公共汽车上,司机负责开车、停车和驾驶,售票员负责门的开门、关门和售票。基本操作规则是只有停车后售票员才能开门。只有售票员关门后司机才能开车。汽车初始状态处于行驶之中。当只有1个司机、2个售票员、2个门、每个售票员负责一个门时的协调操作。请使用P、V原语实现售票员与司机之间的协调操作说明每个信号量的含义、初值和值的范围。
解答
semaphore door1 = 1, door2 = 1; // 司机可以看到的关的门
semaphore full1 = 0, full2 = 0; // 售票员可以开的门的数量
void driver() {
P(door1), P(door2);
run the bus;
drive;
stop the bus;
V(full1), V(full2);
}
void seller1() {
P(full1);
open the door1;
close the door1;
V(door1);
sell the ticket;
}
void seller2() {
P(full2);
open the door2;
close the door2;
V(door2);
sell the ticket;
}
练习9:医生 - 患者 问题
题目
一医院门诊部的候诊室最多允许10名患者进入,若多于10人,则患者必须在门外等待,否则可进入候诊室候诊。门诊部共有3名医生,可同时给患者看病。当候诊室无等待的患者时,医生需等待患者的到来;当医生看病完成时,叫下一个等待的患者。当三个医生都在忙时,患者需等待,患者看病完成后离开候诊室。若把患者和医生看病的过程分别看作进程,试回答下列问题:
(1)用信号量管理这些进程时,应怎样定义信号量,写出信号量的初值及信号量的含义;
(2)根据所定义的信号量,用P、V原语描述患者进程和医生进程的活动,以保证它们能正确地并发执行
解答
semaphore doc_num = 3; // 实现患者间对医生的互斥访问
semaphore room = 10; // 实现患者间对候诊室位置的互斥访问
semaphore doc = 3; // 现在有空的医生数量
semaphore ill = 0; // 现在能看的病人数量
void doctor() {
P(ill);
see a illness;
V(doc);
}
void illness() {
P(room);
V(ill);
P(doc);
P(doc_num);
see a doctor;
V(doc_num);
V(room);
}
练习10:银行取号问题
题目
某银行有人民币储蓄业务,由n个柜员负责。有1台取号机,每天最多放200个号。每个顾客进入银行后先取一个号,若有人取号则需等他人取完后才能取,取到号后等待叫号,当一个柜员人员空闲下来就叫下一个号。试用P、V操作正确编写柜台人员和顾客进程的程序。
解答
semaphore mutex = 1; // 实现顾客对取号机的互斥访问
semaphore empty = 200; // 号数,实现顾客对号数的互斥访问
semaphore full = 0; // 等待叫号的人
semaphore sell = n; // 有空的柜员数
void customer() {
P(mutex);
get a number;
P(empty);
V(mutex);
V(full);
waite to be served;
P(sell);
be served;
V(empty);
}
void servers() {
P(full);
serve the customer;
V(sell);
}
练习11:安全岛通车问题
题目
在南开大学至天津大学间有一条弯曲的路,每次只允许一辆车通过,但中间有小的安全岛M(同时允许两辆车),可供两辆车在已进入两端小车错车,试用P,V实现车辆有序通过。
解答
semaphore M = 2; //实现对安全岛的互斥访问
semaphore K = L = 1; // 表示两边道路
// 从N到T的车
// 先申请安全岛M,再申请这边的道路K
P(M);
P(K);
// 从N到M
V(K);
P(L);
V(M);
// 从M到T
V(L);
// 从T到N的车
//先申请安全岛,再申请这边的道路L
P(M);
P(L);
//从T到M
V(L);
P(K);
V(M);
//从M到N
V(K);
练习12:自行车工人合作问题
题目
设自行车生产线上有一个货架,其中有N(N≥3)个位置,每个位置可存放一个车架或一个车轮;又设有3名工人,车架工每次生产一个车架并放在货架上,轮胎工每次生产一个轮胎并放在货架上,组装工每次从货架上取一个车架两个车轮组装成自行车。使用信号量P、V操作实现三名工人的有序合作。
解答
semaphore empty = N;// 货架上的空位
semaphore weel = 0; //车轮
semaphore frame = 0; //车架
semaphore s1 = N - 2; // 车架最大值
semaphore s2 = N - 1; //车轮最大值
void frameworker() {
do {
produce a frame;
P(empty); // 检测是否还有空位置
P(s1); // 检测车架是否还有空位
put a frame on shelf;
V(frame); //车架数量+1
} while(1);
}
void weelworker() {
do {
produce a weel;
P(empty); // 检测是否还有空位
P(s2); // 检测车轮是否还有空位
put a weel on shelf;
V(wheel); // 车轮数+1
} while(1);
}
void assembleworker() {
do {
P(frame); // 检测是否有车架
get a frame from shelf;
V(empty); // 空位+1;
V(s1); // 车架空位+1;
P(wheel);
P(wheel);
get two weels from shelf;
V(empty);
V(empty);
V(s2);
V(s2);
assemble a bike;
} while(1);
}