CH5 Process Synchronization
文章目录
I. 识别临界区(Race condition)
1. producer–consumer problem用计数器counter会产生的问题
1)producer
while(true)
{
/*produce an item and put in next_produced */
while(counter==BUFFER_SIZE); /*do nothing*/
buffer[in]=next_produced;
in=(in+1)%BUFFER_SIZE;
counter++;
}
2)consumer
while (true){
while (count == 0); // do nothing
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
/*consume the item in next_consumed*/
}
本质问题:并发执行程序代表着,进程在任意的指令流上都有可能被随时中断切换
2. 练习题
题目:有两个并发进程P1和P2,共享初值为1的变量x。P1对x加1,P2对x减1。加1和减1操作的指令序列分别如下所示。
两个操作完成后,x的值是(c)。
A. 可能为-1或3 B. 只能为1
C. 可能为0、1或2 D. 可能为-1、0、1或2
解:
① P1和P2并行运行
P1: x=1,R1=x, R1=R1+1=x+1=2 P2: x=1,R2=x, R2=R2-1=x-1=0
以上是两个进程并发而进行的步骤,而进程的最后一步无法并发进行,因而会出现:
R1→x=2,R2→x=0所以,两个操作完成后,x的值可能为2,也可能为0
②P1先执行,P2后执行
P1: x=1,R1=x=1,R1=R1+1=1+1=2,x=R1=2 P2: x=1,R2=x=2,R2=R2-1=2-1=1,x=R2=1
所以,两个操作完成后,x的值为1
③P2先执行,P1后执行
P2: x=1,R2=x=1,R2=R2-1=1-1=0,x=R2=0 P1: x=0,R1=x=0,R1=R1+1=0+1=1,x=R1=1
所以,两个操作完成后,x的值也为1
所以综合以上三种情况可知x的值可能为0、1或2
II. 软件互斥方法
Solution to critical section problem(三大原则):
①mutual exclusion
②progress(空闲让进):临界区空闲的时候,可以让请求进入临界区的进程立即进入临界区
③bound waiting(有限等待):对于请求访问的进程,应该保证在有限时间内进入临界区(相当于对于请求访问的进程不可以发生饥饿,就是一直进不去)
1. 单标志法
1)实现方法
2)问题
假如P0一直不访问临界区(此时turn=0),P1就无法访问临界区,违反了progress的原则
- P0,P1是否可以互斥的进入临界区?
- P0,P1是否会发生“饥饿”现象?
- P0,P1是否会发生“死锁”现象?
2. 双标志先检查法
1)实现方法
注意:开始的时候 flag[0]=flag[1]=FALSE
2)问题
- P0,P1是否可以互斥的进入临界区?
- P0,P1是否会发生“饥饿”现象?
- P0,P1是否会发生“死锁”现象?
3. 双标志后检查法
1)实现方法
2)问题
-
P0,P1是否可以互斥的进入临界区?
-
P0,P1是否会发生“饥饿”现象?
-
P0,P1是否会发生“死锁”现象?
死锁一定强调的是都在等待对方进程来解锁
4. 时间片改进算法
1)实现方法
2)问题
-
P0,P1是否可以互斥的进入临界区?
-
P0,P1是否会发生“饥饿”现象?
会产生“饥饿”,因为flag[0]=flag[1]=true的时候,两个都进入了while循环,假如延迟时间设置的一样,两个进程同步切换,那就会一直在这个循环里面。
-
P0,P1是否会发生“死锁”现象?
5. Dekker’s Algorithm(有点麻烦,但是解决了问题)
6. Peterson Algorithm(暂时比较完美的解法)
P0:
flag[0]=true; //我想用
turn=1 //但是您先用
while(flag[1]&&turn==1); //您要是想用,那您先用
critical section
flag[0]=false //我用完了
基本思想:
- P0,P1是否可以互斥的进入临界区?
- P0,P1是否会发生“饥饿”现象?
- P0,P1是否会发生“死锁”现象?
7. 练习题
1)区分死锁和饥饿(变形的读者写者问题)
选B。
-
P1,P2是否会发生“饥饿”现象?(注意,可能有多个P1或者多个P2,比如读者写者问题)
解释:按照序列①(x1=0),②(c1=1,y=0)⑦(x2=0)⑧(c2=1,y=-1,P2阻塞)③(x1=1)
此时又来一个P1‘:①(x1=0),②(c1=2),③(x1=1)④(x1=0)⑤(c1=1,不执行V(y)的操作)⑥(x1=1)
所以可以看出,P2必须等所有在P1后面来的P1’走了以后才可以从阻塞对面里面出来,如果无限个P1来了,P2就排不上队了(相当于P1那些小鳖孙儿们不停的插队)
-
P1,P2是否会发生“死锁”现象?
解释:是否产生死锁的关键在于,会不会出现,两个人都阻塞然后还要等待对方来解锁。这里的操作序列是先让P2等待,当时P2等待的时候,P1是可以给他解锁的。不会出现两个都在等待的情况。
2)期中考试原题
此题答案选A
-
P0,P1是否可以互斥的进入临界区?
解释:按照①②③④⑤⑥执行
-
P0,P1是否会发生“饥饿”现象?
解释:按照①③②④的顺序执行,进程P0不需要进程P1的来解锁,对P1也不需要进程P0来解锁。只是这个序列会导致整个程序处于一个循环状态
-
P0,P1是否会发生“死锁”现象?
解释:死锁的话,指的是,进程P0和P1都陷入一个忙等状态,要打开这个忙等状态必须要对方程序来解锁(比如双标志后检查法)
3)上课同学提出的算法
-
P0,P1是否可以互斥的进入临界区?
解释:思路就是让flag[0],flag[1]都先变成true,然后因为进入critical section之前的是要过掉这个if,跳出这个if的关键在于flag[0]和flag[1]的取值不一样。如果两个都变成true了以后让一个进程进入这个if,改变其中的一个flag,构成跳出if的条件,让对方程序进入critical section,然后自己也可以进入critical section。序列如下:flag[0]=flag[1]=false,
6,20,7(没反应),21(没反应),8,22,9(flag[1]=true),23(flag[0]=true),10(进入P0的if循环),12,13,7(flag[0]=false),24,P1进入critical section。8,9(flag[1]=true),10(跳出if循环),P0进入临界区
-
P0,P1是否会发生“饥饿”现象?
III. 硬件实现互斥方法(Synchronization Hardware)
1. 关闭中断(只能运用单处理器系统)
含义:就是不让中断出现,这样就不会有软件方法中交替中断的情况
缺点:过于鲁莽。因为有的进程如果在临界区的时间很长的话,中断是一个很好的选择。关中断还是适用于进入临界区的时间较短的时候(关了马上可以开)
2. 硬件方法(atomic hardware instructions)
atomic hardware instructions:Atomic = non-interruptible
优点:运用软件实现方法的问题其实根本在于“上锁”和“检查”是分开的,比如双标志先检查法,进入临界区以前先检查后上锁而导致了中断切换间的死锁问题:
所以如果“上锁”和“检查”变成了一气呵成的原子操作,且不会被中断打扰,那么实现起来就简单且容易操作
1)test_and_set()
- 程序使用test_and_set()指令实现互斥的逻辑:进程首先都会拿到一个lock,如果lock=F的话,那么就可以进入临界区。
任何一个程序的lock无非就两种情况:
① lock=T:lock=T,就把lock继续变成T然后在while里面盘旋
② lock=F:lock=F,可以不在while里面循环,但是lock还是会变成T,这时候其他来的进程lock就是T,进不来。
do
{
while(test_and_set(&lock)); /*aquire and set lock(相当于返回lock本来的值然后在set新的值)*/
/* critical section */
lock=F; //解锁,把权力给其他程序
/* remainder section */
}while(T)
test_and_set()指令实现的代码:
bool test_and_set(bool *target)
{
bool old = *target; //用old存放原来lock的值
*target = T; //“上锁”:无论之前是否已经加锁,都需要将lock变成TRUE
return old; //“检查”:函数返回之前的lock值
}
-
问题:有可能会出现starvation,因为进程只有抢到lock=FALSE才可以进入临界区,所以进程抢不到lock=FALSE就进不去临界区。
-
解决:
2)compare_and_swap()
compare_and_swap程序实现逻辑:lock=0代表拿到锁了
do
{
while (compare_and_swap(&lock, 0, 1) != 0) ; /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
}while(true);
compare_and_swap() 实现方式:
int compare_and_swap(int *value, int expected, int new_value)
{
int old = *value; //“检查”:temp还是先把old的值先获取了
if (*value == expected) *value = new_value; //“上锁”:如果可以进入临界区就上锁
return old;
}
3. spining lock
while(!available) ; /*busy waiting*/
-
缺点:当一个进程在临界区的时候,其他任何的进程必须在进入临界区以前进入busy waiting的状态,等到lock可以用为止。这样的busy waiting会浪费CPU周期。
-
优点:当进程等待lock的时候可以不用进行context switching。所以当进程被锁的时间很短的时候还是有利于减小程序时间上的切换开销的,适用于多处理器系统(multiprocessor)
4. 练习题
IV. 信号量(Semaphore)
1. 定义信号量
每一个信号量都有一个正数value和一个进程链表list。当一个进程必须等待信号量时,就要被添加到进程链表
typedef struct
{
int value;
struct process *list;
}semaphore;
2. 信号量的实现
P操作的实现:
wait(semaphore *S)
{
S->value--;
if (S->value < 0){
/*add this process to S->list;*/
block();
}
}
V操作实现:
signal(semaphore *S)
{
S->value++;
if(S-value>=0){
/*remove a process P from s->list*/
wakeup(P); //重新启动P(之前时阻塞)的进行
}
}
V. 经典同步互斥例子
1. 互斥问题
-
基本情景:同一个时间段只允许一个进程访问临界区
-
实现方法:
semaphore mutex=1;
P1()
{
...
P(mutex); /*“加锁”:使用临界区前加锁/
/*crtical section*/
V(mutex); /*“解锁”:使用完临界区以后解锁*/
}
P2()
{
...
P(mutex); /*“加锁”:使用临界区前加锁/
/*crtical section*/
V(mutex); /*“解锁”:使用完临界区以后解锁*/
}
注:对于不同的临界资源要设置不同的信号量
2. 同步问题
- 基本情景:两个并发运行的进程,P1有语句S1,P2有语句S2,要求在S1执行后才能执行S2。(相当于假如我们现在需要煮饭,我们要在开火之前要先放米和水,一定是有一个牵制关系在)
- 解决方式:在“前操作”之后V,在“后操作”之前P
semaphore S=0
P1:
S1;
signal(S);
P2:
wait(S); //如果S=0,wait(S),P2直接阻塞,必须需要P1才可以解锁
S2;
3. 同步+互斥问题
1)Bounded-Buffer Problem
- 信号量初始化
-
empty信号量:用来限制buffer满了(empty=0),但是producer仍然要往里面放东西的情况
-
full信号量:用来限制buffer空的(full=0),但是consumer还是要从buffer里面取东西的情况
-
mutex信号量:对于多个producer或者多个consumer同时操作的时候,假设n=5,empty=3。比如,现在producer1进程先执行到第6行,
Buffer[1]=item1
:do { /* produce an item in next_produced */ ... wait(empty); //empty=2 ... Buffer[in] = next_produced ; //in=1 in = (in+1)% BUFFER_SIZE; ... signal(full); ... }while (true);
然后producer1发生中断,producer2进程也执行同样的程序到第6行。因为in没有变,
Buffer[1]=item2
,所以producer2生产的item会在Buffer[1]=item
这里产生覆盖do { /* produce an item in next_produced */ ... wait(empty); //empty=1 ... Buffer[in] = next_produced ; //in=1 in = (in+1)% BUFFER_SIZE; ... signal(full); ... }while (true);
-
实现方式:
同步关系:当buffer满,生产者不可以再生产;当buffer空,消费者不可以再消费。
互斥关系:两个生产者或两个消费者之间不可以同时对buffer进行装入/取出操作。
-
producer:生产东西,如果满了(empty=0)不生产
do { /* produce an item in next_produced */ ... wait(empty); //if empty=0,block wait(mutex); Buffer[in] = next_produced ; in = (in+1)% BUFFER_SIZE; ... signal(mutex); signal(full); }while(T)
-
consumer:消费东西,如果是空的就不消费(full=0)
do { wait(full); //if full=0,block wait(mutex); ... next_consumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; ... signal(mutex); signal(empty); /*consume the item in next_consumed */ }while(T)
-
wait(empty)和wait(mutex)可以互换吗?
不可以,会产生死锁问题。
按照①②③④的顺序执行就会锁死
2)多生产者消费者问题
互斥关系:爸爸和妈妈之间,儿子和女儿之间对盘子的互斥。
同步关系:
- 爸爸和女儿之间的同步(只有爸爸放了苹果,女儿才可以拿)
- 儿子和妈妈之间(只有妈妈放了橘子,儿子才可以拿)
- 只有盘子为空的时候,父亲或妈妈才可以往盘子放东西
Initialization:
semaphore orange=0;
semaphore apple=0;
semaphore mutex=1;
爸爸F:
do
{
wait(mutex); //if(mutex=0),代表盘子是被占了的,需要被block
/*put the apple in the plane*/
signal(apple)
}while(T)
妈妈M:
do
{
wait(mutex); //if(mutex=0),代表盘子是被占了的,需要被block
/*put the orange in the plane*/
signal(orange)
}while(T)
女儿D:
do
{
wait(apple);
/*retrieve the apple in the plane*/
signal(mutex)
}while(T)
儿子S:
do
{
wait(orange);
/*retrieve the orange in the plane*/
signal(mutex)
}while(T)
3)Dining-Philosophers Problem
情景:
-
Philosophers spend their lives alternating thinking and eating
-
Don’t interact with their neighbors, occasionally try to pick up 2 chopsticks (one at a time) to eat from bowl
-
Need both to eat, then release both when done
问题分析:会形成死锁
semaphore chopstick[5]={1,1,1,1,1}
do
{
wait(chopstick[i]); //拿左边筷子
wait(chopstick[(i+1)%5]) //拿右边筷子
/*eating*/
signal(chopstick[i]); //放下左边筷子
wait(chopstick[(i+1)%5]) //放下右边筷子
/*thinking*/
}while(T)
解决方法:
-
最多允许四位哲学家进餐
semaphore chopstick[4]={1,1,1,1} do { wait(chopstick[i]); //拿左边筷子 wait(chopstick[(i+1)%5]) //拿右边筷子 /*eating*/ signal(chopstick[i]); //放下左边筷子 wait(chopstick[(i+1)%5]) //放下右边筷子 /*thinking*/ }while(T)
-
奇数号的哲学家先拿左边的筷子,然后再拿右边的筷子;偶数号的哲学家先拿起右边的筷子,然后再拿左边的筷子
semaphore chopstick[5].value={1,1,1,1,1} Pi=0,2,4 do { wait(chopstick[(i+1)%5]); wait(chopstick[i]); /*eating*/ signal(chopstick[i+1]%5); signal(chopstick[i]); /*thinking*/ }while(T)
Pi=1,3 do { wait(chopstick[i]); wait(chopstick[i+1]); /*eating*/ signal(chopstick[i]); signal(chopstick[i+1]); /*thinking*/ }while(T)
-
把取筷子也变成一个互斥的过程
例:①比如0号哲学家先拿起左边和右边的筷子,然后3号哲学家也可以顺利的拿起左边和右边的筷子(mutex可以让三号哲学家进去)
②比如还是0号哲学家先拿起左边和右边的筷子,然后1号哲学家要拿筷子,但是被chopstick[1]挂掉了
Pi=0,1,2,3,4 semaphore chopstick[5].value={1,1,1,1,1}; semaphore mutex=1; Pi() { do { wait(mutex); wait(chopstick[i]); wait(chopstick[(i+1)%5]); signal(mutex); /*eating*/ signal(chopstick[i]); wait(chopstick[(i+1)%5]); /*thinking*/ }while(T) }
-
第四个进程先拿起4再拿起0,相当于如果0,1,2,3这四个哲学家同时拿起了左边的筷子,4虽然拿不起右边的筷子,但是只要3拿起右边的筷子然后放下左边和右边的筷子就可以解锁
process Pi( ) (i = 0, 1, 2, 3) { while (true) { wait (chopstick[i]); wait (chopstick[i+1]); eating ; signal (chopstick[i]); signal (chopstick[i+1]); thinking ; } }
process P4( ) { while (true) { wait (chopstick[0]); //先拿右边的筷子 wait (chopstick[4]); eating ; signal (chopstick[0]); signal (chopstick[4]); thinking ; } }