第二章 进程与线程(三)
进程同步
进程同步解决异步问题有。同步亦称制约关系,是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调他们的工作次序而产生的制约关系,进程间的直接制约关系源于他们之间的相互合作。
进程互斥
- 把一个一段时间内只允许一个进程使用的资源称为临界资源。对临界资源的访问必须互斥的进行。互斥亦称间接制约关系。进程互斥指当一个进程访问临界资源时,另一个想要访问该资源的进程必须等待。
- 对于临界资源的访问,可以在逻辑上分为四部分:
do
{
entry section;//进入区,负责检查是否可以进入临界区,若可以进入,设置正在访问资源标志,以防止其他进程进入临界区
critical section;//临界区,访问临界资源的代码
exit section;//退出区,负责解除正在访问临界资源的标志
remainder section;//剩余区,做其他处理
}while(true);
临界区是进程访问临界资源的代码段
进入区和退出区是负责实现互斥的代码段
- 遵循原则:
空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
忙则等待:现已有进程进入临界区时,其他试图进入临界区的进程必须等待
有限等待:对请求访问的进程应能保证在有限时间内进入临界区
让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待
软件实现方法
单标志法
两个进程在访问完临界区后会把使用临界区的权限交给另一个进程,也就是说每个进程进入临界区的权限只能被另一个进程赋予。
int turn = 0;//表示当前允许进入临界区的进程号
//p0进程
while(turn != 0);//1
critical section;//2
turn = 1;//3
remainder section;//4
//p1进程
while(turn != 1);//5
critical section;//6
turn = 0;//7
remainder section;//8
turn的初值为0,及刚开始只允许0号进程进入临界区。
若p1先上处理机运行,则会之一卡在代码5,直到p1的时间片完发生进程调度,切换p0上处理及运行。代码1不会卡住p0,p0可以正常访问临界区,在p0访问临界区期间即使切换回p1,p1依然会卡在代码5,只有p0退出临界区将turn改为1后,p1才能进入临界区。
但是单标志法违背了“空闲让进”原则。
双标志先检查法
设置一个布尔型数组flag[],数组中各个元素用来标记各进程想要进入临界区的意愿。每个进程在进入临界区之前先检查当前有没有别的进程想要进入临界区,如果没有,则把自身对英里的标志设为true,之后访问临界资源。
bool flag[2];//表示进入临界区意愿的数组
flag[0] = false; flag[1] = false;//刚开始两个进程都不想进入临界区
//p0进程
while(flag[1]);//1
flag[0] = true;//2
critical section;//3
flag[0] = false//4
//p1进程
while(flag[0]);//5
flag[1] = true;//6
critical section;//7
flag[1] = false//8
若按照1,5,2,6,3,7…的顺序执行,p0和p1将会同时访问临界区,因此双标志先检查法违反了“忙则等待”原则。
原因在于进入区的“检查”和“上锁”两个处理不是一气呵成的,“检查后”“上锁前”可能发生进程切换。
双标志后检查法
双标志先检查法的改版,先“上锁”后“检查”
bool flag[2];//表示进入临界区意愿的数组
flag[0] = false; flag[1] = false;//刚开始两个进程都不想进入临界区
//p0进程
flag[0] = true;//1
while(flag[1]);//2
critical section;//3
flag[0] = false//4
//p1进程
flag[1] = true;//5
while(flag[0]);//6
critical section;//7
flag[1] = false//8
若按照1,5,2,6…的顺序执行,p0和p1都无法进入临界区。
虽然解决了”忙则等待“问题,但又违背了”空闲让进“和”有限等待“原则,会导致饥饿
Peterson算法
结合双标志法,但标志法的思想。如果双方都想进入临界区,即可以尝试”孔融让梨“
bool flag[2];//表示进入临界区意愿的数组
flag[0] = false; flag[1] = false;//刚开始两个进程都不想进入临界区
int turn = 0;//允许进入临界区的进程号
//p0进程
flag[0] = true;//1
turn = 1;//2
while(flag[1]&&turn==1);//93
critical section;//4
flag[0] = false//5
//p1进程
flag[1] = true;//6
turn = 0;//7
while(flag[0]&&turn==0);//8
critical section;//9
flag[0] = false//10
Peterson算法用软件方法解决了进程互斥问题,遵循了空闲让进,忙则等待,有限等待三个原则,但依然未解决让权等待的原则。
硬件实现方法
中断屏蔽方法
利用“开/关中断指令”实现(与原语的实现思想相同),某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换。
优点:高效,简单
缺点:不能用于多处理机,只适用操作系统内核进程,不适用于用户进程。因为“开关中断指令”只能在内核态运行,是特权指令。
Test And Set指令(TS 或TSL)
用硬件实现,执行的过程不允许被中断,只能一气呵成。
//bool型变量lock表示当前临界区资源是否被加锁,true表示加锁
bool TestAndSet(bool *lock)
{
bool old;
old = *lock;//用old存放lock原来的值
*lock = true;//无论之前是否加锁,lock都为true
return old;
}
while(TestAndSet(&block));//上锁检查
临界区代码...
lock = false;
剩余代码段...
若刚开始lock是false,则TestAndSet返回的old为false,while条件不满足,跳过循环进入临界区。若刚开始是true,则TestAndSet返回的old为true,while条件满足,会一直循环,直到当前访问临界区的进程“解锁”。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞,适用于多处理机环境。
缺点:不满足让权等待,会导致忙等。
Swap指令(Exchange或XCHG)
//Swap指令的作用是交换两个变量的值
Swap(bool *a,bool*b)
{
bool temp;
temp = *a;
*a = *b;
*b = temp;
}
bool old = true;
while(old == true)
Swap(&lock,&old);
临界区代码...
lock = false;
剩余区代码段
互斥锁
临界区最简单的工具就是互斥锁。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数acquire()获得锁,函数release()释放锁。
每个互斥锁有一个布尔变量available,表示锁是否可用。如果锁是可用的,调用acquire()会成功,且锁不再可用。当一个进程试图获取不可用锁时,会被阻塞,知道锁被释放。
acquire(){
while(!avaiable);//忙等待
available = false;//获得锁
}
release(){
available = true;//释放锁
}
acquire() 或 release() 的执行必须是原子操作,因此互斥锁采用硬件机制实现。互斥锁的缺点是忙等待。互斥锁通常用于多处理机系统,一个线程可以在一个处理机上等待,不影响其他线程的执行。
需要连续循环忙等的互斥锁称为自旋锁,如TSL,Swap,单标志法。
缺点:需忙等,进程时间片用完时才下处理机,违反让权等待。
优点:等待期间不用切换进程,多处理器系统中,若上锁时间短,则等待代价很低。常用于多处理器系统,一个核忙得,其他核照常工作,并快速释放临界区。不太适用于单处理器系统,忙等的时间过程中不可能解锁。
信号量机制
用户进程可以通过使用操作系统提供的一个原语来对信号量进行操作,从而很方便的实现了进程互斥,进程同步。
信号量是一个变量,可以用一个信号量来表示系统中某资源的数量。
原语:wait(s)原语 和 signal(s)原语,简称P,V操作,P(s),V(s)。
整形信号量
用一个整数类型的变量作为信号量,用来表示系统中某资源的数量。
存在的问题:不满足“让权等待”,会发生忙等。
int S = 1;//初始化整形信号量S,表示当前系统中可用资源数
void wait(int S){//wait原语,相当于“进入区”
while(S < 0);//如果资源不够,一直循环等待
S = S - 1;//如果资源数够用,占用一个资源
}
void signal(int S){//signal原语,相当于“退出区”
S = S + 1;//使用完资源后,释放资源
}
记录型信号量
用记录型数据结构表示的信号量。
typedef struct{//记录型信号量的定义
int value;//剩余资源数
struct process *L;//等待队列
}Semaphore;
void wait(Semaphore S){//某进程需要使用资源时,通过wait原语申请
S.value--;
if (S.value < 0)
block(S,L);
}
void signal(Semaphore S){//进程使用完资源后,通过signal原语释放
S.value++;
if(value > 0)
wakeup(S,L);
}
信号量机制实现进程互斥
1.分析并发进程的关键活动,划动临界区。2.设置互斥信号量mutex,初值为1。
Semaphore mutex = 1;//初始化信号量
P(1){
...
P(mutex);//使用临界资源
临界区代码...
V(mutex);//释放临界资源
}
针对不同的临界资源需要设置不同的互斥信号量。P,V操作必须成对出现。缺P不能保证临界资源的互斥访问,缺V会导致资源永不被释放,等待进程永远不会被唤醒。
信号量机制实现进程同步
让本来异步并发的进程相互配合,有序推进。
分析什么地方需要实现“同步关系”,即保证“一前一后”执行的两个操作。
设置同步信号量S,初值为0;
在“前操作”之后执行V(S),在“后操作”之前执行P(S).
生产者—消费者问题
系统中。有一组生产者进程和一组消费者进程,生产者每次生产一个或多个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品投入使用。
生产者,消费者共享一个初始为空,大小为n的缓冲区,只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。只有缓冲区没空时,消费者才能从中取出产品,否则必须等待。缓冲区是临界资源,各进程必须互斥的访问。
Semaphore mmutex = 1;//互斥信号量,实现对缓冲区的互斥访问
Semaphore empty = n;//同步信号量,表示空闲缓冲区的数量
Semaphore full = 0;//同步信号量,表示产品的数量
producer(){
while(1){
生产一个产品...
P(empty);
P(mutex);
把产品放入缓冲区...
V(mutex);
V(full);
}
}
consumer(){
while(1){
P(full);
P(mutex);
从缓冲区取出产品...
P(mutex);
V(empty);
}
}
实现互斥的P操作一定要在实现同步的P操作之后。
多生产者—多消费者问题
问题描述:桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。
Semaphore mutex = 1;//实现互斥访问盘子
Semaphore apple = 0;//盘子中有几个苹果
Semaphore orange = 0;//盘子中有几个橘子
Semaphore plate = 1;//盘子中还可以放几个水果
dad(){
while(1){
准备一个苹果...
P(plate);
P(mutex);
放入盘子...
V(mutex);
V(apple);
}
}
mom(){
while(1){
准备一个橘子...
P(plate);
P(mutex);
放入盘子...
V(mutex);
V(orange);
}
}
daughter(){
while(1){
P(apple);
P(mutex);
吃苹果...
V(mutex);
V(plate);
}
}
son(){
while(1){
P(orange);
P(mutex);
吃橘子...
V(mutex);
V(plate);
}
}
吸烟者问题
问题描述:有三个抽烟者和一个供应者。每个抽烟者不停地卷烟抽,组成一根烟需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个有烟草,第二个有纸,第三个拥有胶水。供应者无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,那么供应者可以继续提供另外两种材料,如此重复(让三个抽烟者轮流地抽烟)。
Semaphore offer1 = 0;//桌上组合一的数量(烟草,纸)
Semaphore offer2 = 0;//桌上组合一的数量(烟草,胶水)
Semaphore offer3 = 0;//桌上组合一的数量(胶水,纸)
Semaphore finish = 0;//抽烟是否完成
int i = 0;//用于实现“三个抽烟者轮流抽烟”
provider(){
while(1){
if(i == 0)
V(offer1);
else if(int == 1)
V(offer2);
else if(i == 3)
V(offer3);
i = (i+1)%3;
P(finish);
}
}
smoker1(){
while(1){
P(offer1);
V(finish);
}
}
smoker2(){
while(1){
P(offer2);
V(finish);
}
}
smoker3(){
while(1){
P(offer3);
V(finish);
}
}
读者写者问题
要求:1.允许多个读者可以同时对文件执行读操作。2.只允许一个写进程往文件中写信息。3.任一写者在完成写操作之前不允许其他读者或写者工作。3.写者执行操作前,应让已有的读者和写者全部退出。
Semaphore rw = 1;//用于实现对共享文件的互斥访问
int count = 0;//记录当前有几个读进程在访问进程
Semaphore mutex = 1;//用于保证对count变量的互斥访问
Semaphoore w = 1;//用于实现“写优先”
writer(){
while(1){
P(w);//写文件
P(rw);//写之前加锁
写文件...
V(rw);//写完解锁
V(w);
}
}
reader(){
while(1){
P(w);
P(mutex);//各读进程进行互斥访问count
if(count == 0)//由第一个读进程加锁
P(rw);
count++;//访问文件进程数+1
V(mutex);
V(w);
读文件...
P(mutex);//各读进程互斥访问count
count--;//访问文件的读进程--
if(count == 0)
V(rw);//最后一个读进程读完解锁
V(mutex);
}
}
哲学家进餐问题
有五个哲学家围在一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和物质筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两支筷子时才能进餐。进餐完毕后,放下筷子继续思考。
Semaphore chopstic[5] = {1,1,1,1,1};
Semaphore mutex = 1;//互斥的取走筷子
P(){
while(1){
P(mutex);
P(chopsetick[i]);//拿左筷子
P(chopsetick[(i+1)%5]);//拿右筷子
V(mutex);
吃饭...
V(chopsetick[i]);//拿左筷子
V(chopsetick[(i+1)%5]);//拿右筷子
思考...
}
}
管程
为了解决信号量机制编程麻烦,易出错的问题,引入了管程。
组成:
局部于管程的共享数据结构说明;
对该数据结构进行操作的一组过程;
对局部于管程的共享数据设置初始值的语句;
一个管程名字。
基本特征:
局部与管程的数据只能被局部与管程的过程所访问;
一个进程只有通过调用管程内的过程才能进入管程访问共享数据;
每次仅允许一个进程在管程内执行某个内容。
由编译器负责实现各进程互斥的进入管程中。