2.3同步与互斥
下面的代码只是用于理解其中的逻辑,并不是计算机编译器中的代码就是这些代码。
重点
PV操作
管程
生产者消费者问题
多生产者和多消费者问题
问题
为什么引入进程同步概念
因为进程具有异步性(进程各自独立,以不可预知的速度推进)。为了协调进程之间的相互制约关系,引入了进程同步的概念。
管程中的wait、singal 和 同步中的P V操作的区别
x.wait:当x对应的条件不满足时,正在调用**管程的进程调用x.wait将自己插入x条件的等待队列,并释放管程。**此时其他进程可以使用该管程。
x.signal: x对应的条件发生了变化,则调用x.signal,唤醒一个因x条件而阻塞的进程。
条件变量和信号变量的比较
相似点:条件变量的wait/signal操作类似于信号量的P/V操作,可以实现进程的阻塞唤醒。
不同点:
- 条件变量是没有值的,仅实现了”排队等待“功能;而信号量是”有值“的。
- 信号量的值反映了剩余资源数,在管程中,剩余资源数用共享数据结构记录。
管程的概念
当共享资源用共享数据结构表示时,资源管理程序可用对该数据结构进行操作的一组过程来表示。把这样一组相关的数据结构和过程一并归为管程。
Hansan为管程所下的定义是:“一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。”由定义可知,管程由以下部分组成:
- 局部于管程的共享变量说明。
- 该数据结构进行操作的一组过程。
- 对局部于管程的数据设置初始值的语句;
- 管程名称。
管程的引入是为了解决临界区分散所带来的管理和控制问题。
术语
- 临界资源 :在一段时间内只允许一个进程访问的资源。
- 临界区:进程访问临界资源的那段代码
- 忙等待:进程因需要等待资源,同时又占用处理机
2.3.1 同步与互斥的基本概念
临界资源
在一段时间内只允许一个进程访问的资源。又称独占资源。
- 常见的临界资源:系统中的大多数物理设备:如打印机、磁带机。还有栈和变量、表格。
- 访问方式:互斥访问。
- 临界资源不能并发的被多个程序占用,其他资源可以。
临界区
临界区是进程访问临界资源的那段代码。《是代码,是代码,是代码》
- **进入区。**为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临
界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区。- **临界区。**进程中访问临界资源的那段代码,又称临界段。
- **退出区。**将正在访问临界区的标志清除。
- 剩余区。代码中的其余部分。
注:主要是 进入区、临界区、退出区。
进入区和退出区 是负责实现互斥的代码段。
进程同步
在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。 我们把异步环境下的一组并发进程因直接制约而互相发送消息、进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。
因为进程具有异步性(进程各自独立,以不可预知的速度推进)。为了协调进程之间的相互制约关系,引入了进程同步的概念。
也称直接制约关系。
进程互斥
进程互斥是多道程序系统中进程间存在的一种源于资源共享的制约关系,主要是由被共享资源的使用性质所决定的。、
禁止两个进程同时进入临界区。
也称间接制约关系。
需要遵循以下机制:
- 空闲让进 :临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
- 有限等待:对请求访问的进程,应保证能在有限时间进入临界区。
- 让权等待:当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。
2.3.2 实现临界区互斥的基本方法
软件实现方法
算法1:单标志法
思想:
该算法设置一个公用整型变量turn,用于指示被允许进入临界区的进程编号,即若turn = 0,则允许P0,进程进入临界区。
该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区、若某个进程不再进入临界区,则另一个进程也将无法进入临界区(违背“空闲让进"),造成资源利用不充分。
P0顺利进入临界区并从临界区离开,则此时临界区是空闲的,但P并没有进入临界区的打算,turn = 1一直成立,P0就无法再次进入临界区.(一直被while死循环困住:)。缺点
- 违背“空闲让进
- 会造成死循环
代码
int turn = 0;//公共整形变量 /* p0 */ while (turn != 0); //临界区 turn =1; //剩余区
/* p1*/ while (turn != 1); //临界区 turn =0; //剩余区
算法2:双标志法先检查
思想
该算法的基本思想是在每个进程访问临界区资源之前,先查临界资源是否正被访间,若正被访间,该进程需等待,否则,进程才进入自己的临界区。
为此,设置一个数据 flag[i],如:
- 第 i个元素值为FALSE,表示Pi进程未进入临界区
- 值为TRUE.表示Рi进程进入临界区。
优点
- 不用交替进入,可连续使用
缺点
- 可能同时进入临界区,违背“忙则等待”
进入区的检查和修改操作不能一次进行,如果是源语即可避免这个问题。
代码
// int flag[i] ; // Pi进程 while (flag[j]);//进入区 flag[i] = TRUE;//进入区 //临界区 flag[i] = FALSE; //退出区 //剩余区
// Pj进程 while (flag[i]);//进入区 flag[j] = TRUE;//进入区 //临界区 flag[j] = FALSE; //退出区 //剩余区
算法3:双标志法后检查
思想
算法二先检测对方的进程状态标志,再置自已的标志
在检测和放置中可插入另一个进程到达时的检测操作,会造成两个进程在分别检时进入临界区。为此,算法三先将自已的标志设置为 TRUE.再检测对方的状态,若对方标志为TRUE,则进程等待,否则进入临界区.。
优点
解决了 忙则等待问题。
缺点
- 违背 “空闲让进”
- 违背“有限等待”
- 会导致“饥饿”现象,即二者互相谦让(设置自己的状态后,检查对方的状态,如果对方状态为TRUE,则一直循环等待)
代码
// Pi进程 flag[i] = TRUE;//进入区 while (flag[j]);//进入区 //临界区 flag[i] = FALSE; //退出区 //剩余区
// Pj进程 flag[j] = TRUE;//进入区 while (flag[i]);//进入区 //临界区 flag[j] = FALSE; //退出区 //剩余区
算法4:Peterson`s Algorithm
思想
为了防止两个进程为进入临界区而无限期等待,又设置了变量 turn,每个进程在先设置自己的标志后再设置 turn标志。这时,再同时检测进程状态标志和允许进入标志,以便保证两个进程同时要求进入临界区时,只允一个进程进入临界区。
具体如下:考虑进程Pi,一旦.设置flag[i] =true,就表示它想要进入临界x,同时turm=j,此时若进程Рj已在临界区中,符合进程Pi的 while循环条件,则Pi不能进入临界区,若pj不想要进入临界区,即 flag[j]= false,循环条件不符合,则Pi可以顺利进入,反之亦然。
本算法的基本思想是算法一和算法三的结合。利用flag 解决临界资源的互斥访问,而利用turn解决“饥饿”现象。
优点
遵循了:
- 空闲让进
- 忙则等待
- 有限等待
利用flag 解决临界资源的互斥访问,而利用turn解决“饥饿”现象。
缺点
- 违背了让权等待原则
代码
// Pi进程 flag[i] = TRUE;//进入区 turn = j;//进入区 while (flag[j] && turn == j);//进入区 //临界区 flag[i] = FALSE; //退出区 //剩余区
// Pj进程 flag[j] = TRUE;//进入区 turn = i;//进入区 while (flag[i] && turn == i);//进入区 //临界区 flag[j] = FALSE; //退出区 //剩余区
四种方法的对比
比较 | 1.单标志法 | 2.双标志法先检查 | 3.双标志后检查 | 4.peterson`s Algorithm |
---|---|---|---|---|
解决 | 使用公共变量的方法解决互斥访问 | 解决“交替进入问题” | 解决“同时进入”(忙则等待)问题 | 解决互斥和“饥饿”问题 |
缺点 | 违背“空闲让进” | 违背“忙则等待” | 违背“空闲让进”和”有限等待“,会产生饥饿现象 | 违背“让权等待” |
硬件实现方法
计算机提供了特殊的硬件允许对一个字中的内容进行检测和修正,或对两个字的内容进行交换等。通过硬件支持实现临界段问题的方法称为低级方法,或称元方法。
后面习题里有对PV操作的描述为 低级进程通信源语
中断屏蔽方法
利用“开/关中断指令”实现
优点
简单、高效
缺点:不适用于多处理机;由于开关中断权限很大,只适合操作系统的内核进程,不适用于用户进程;违背让权等待原则。
硬件指令方法
TestAndSet指令(TS、TestAndSetLock、TSL)
TSL指令使用硬件实现,执行过程中不允许被中断
优点
实现简单;适用于多处理机环境
缺点
违背让权等待原则
TestAndSet实现逻辑
//布尔型共享变量lock表示当前临界区是否被加锁 //true表示已加锁 bool TestAndSet(bool *lock){ bool old = *lock; //old用于存放lock原来的值 *lock = true; //无论之前是否已加锁,都将lock设为true return old; // 返回old原来的值 }
TSL实现互斥
while(TestAndSet(&lock)){ //检查并上锁 //临界区代码 lock = false; //解锁 //剩余区代码 }
Swap指令
Swap指令使用硬件实现,执行过程中不允被中断。
优点
实现简单;适用于多处理机环境
缺点
违背让权等待原则
实现逻辑
Swap(bool *a, bool *b){ bool temp; temp = *a; *a = *b; *b = temp; }
实现互斥
bool key = true; while(key==true){ Swap(&lock,&key); } //临界区代码 lock = false; //剩余区代码
硬件方法的缺点
适用于任意数目的进程,而不管是单处理机还是多处理机:简单、容易验证其正确性。
可以支持进程内有多个临界区,只需为每个临界区设立一个布尔变量。
硬件方法的缺点
进程等待进入临界区时要耗费处理机时间,不能实现让权等待。
从程中随机选择个进入临界区,有的进程可能一直选不上,从而导致“饥饿”现象。
2.3.3 互斥锁
解决临界区最简单的工具就是互斥锁( mutex lock )。一个进程在进入临界区时应获得退出临界区时释放锁。
函数 acquire()获得锁,而函数release()释放锁。
每个互斥锁有一个布尔变量 available,表示锁是否可用。如果锁是可用的,调用acqirue成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
acquire(){
while(!available);//忙等待
available = false;//获取锁
}
release(){
available = true;//释放锁
}
- acquire()或release()的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。
- 互斥锁的主要缺点是忙等待。
- 互斥用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。
2.3.4 信号量——重点
信号量机制是一种功能较强的机制,可用来解决互斥与同步问题,它只能被两个标准wait(S)和l signal(S)访间,也可记为“Р操作”和“V操作”。
原语 是指完成某种功能且不被分割、不被中断执行的操作序列,通常可由硬件来实现
1.整型信号量
整型信号量被定义为一个用于表示资源数目的整型量S, wait和 signal 操作可描述为
wait(S){
while(S<0);//忙等待 -- 违背了 ”让权等待“
S=S-1;
}
signal(S){
S=S+1;
}
2.记录型信号量
记球型信号量机制是一种不存在“忙等”现象的进程同步机制。
除了需要一个用于代表资源数目的整型变量value外,再增加一个进程链表L,用于链接所有等待该资源的进程。记录型信号量得名于采用了记录型的数据结构。
记录型信号量可描述为
typedef struct{
int value;//代表资源数目
struct process *L;//等待队列
} semaphore;
wait(S) 和 signal(S) 如下
void wait(semaphore S){//相当于申请资源
S.value --;// 申请现有资源
if(S.value < 0){
//把该进程加入到S.L 等待队列里
block(S.L);//使进程 从运行态 进入 阻塞态;放弃处理机,并插入到等待队列
//这句说明 其遵循了“让权等待”
}
}
void signal(semaphore S){//相当于释放资源
S.value ++;//释放占有的资源
if(S.value <= 0){//如果 等待队列里有等待的进程,则取出队首的进程
wakeup(P);
}
}
3.利用信号量实现同步
信号量机制能用于解决进程间的各种同步问题。设S为实现进程Р1,P2同步的公共信号量,初值为0。进程P:中的语句y 要使用进程P,中语句x的运行结果,所以只有当语句x执行完成之后语句y才可以执行。其实现进程同步的算法如下:
同步的信号量不一定是0,根据情况需要用户可自己确定。
semaphore S=0;//初始化信号量
P1(){
x;//语句x
V(S);//告诉P2 ,x已经完成
...
}
P2(){
...
P(S);//检查x是否已经完成
y;//检查无误运行y语句
...
}
4.利用信号量实现进程互斥
设S为实现进程P1,P2互斥的信号量,。**由于每次只允许一个进程进入临界区,所以初始值S设为1。**只需把临界区置于P(S)和V(S)之间,即可实现两个进程对临界资源的互斥访问。
算法如下:
互斥信号量的初识值一般为1,不会改变。
semaphore S = 1;
P1(){
...
P(S);//加锁
//p1临界区
V(S);//解锁
}
P2(){
...
P(S);//加锁
//p2临界区
V(S);//解锁
}
- 不同的临界资源需要设置不同的互斥信号量
- PV操作必须成对出现
5.利用信号量实现前驱关系
信号量也可用描述程序之间的前驱关系。图2.10给出了一个前驱图,具中S1,S2,S3,…,S。是最简单的程序段(只有-条语句)。为使各程序段能止佣执行,应设置若干初始值为“0”的信号量。例如,为保证S1→Sz,S1→S:的前驱关系,应分别设置信号量al, a2。同样,为保让S2→S4, S2→S5, S3→S6, S4→S6, S5→S6,应设置信号量bl, b2,c,d, e。
semphore a1=a2=b1=b2=c=d=e=0;//初始化信号量
S1(){
...;
V(a1);
V(a2);//S1已经运行完成
}
S2(){
P(a1);//检查S1是否运行完成
...;
V(b1);
V(b2);//S2已经运行完成
}
S3(){
P(b1);//检查S1是否运行完成
...;
V(c);//S3已经运行完成
}
S4(){
P(b1);//检查S2是否运行完成
...;
V(d);//S4已经运行完成
}
S5(){
P(b2);//检查S2是否运行完成
...;
V(e);//S5已经运行完成
}
S6(){
P(c);//检查S3是否运行完成
P(d);//检查S4是否运行完成
P(e);//检查S5是否运行完成
}
6.分析进程同步和互斥问题的方法步骤
-
关系分析。
找出进程数,并分析之间的同步和互斥关系。
-
整理思路。
根据进程的流程确定P操作、V操作的大致顺序。
-
设置信号量
根据上面两步,设置需要的信号量,确定初值,完善整理。
2.3.5 管程
管程的引入是为了解决临界区分散所带来的管理和控制问题。在没有管程之前,对临界区的访问分散在各个进程之中,不易发现和纠正分散在用户程序中的不正确使用P,V操作等问题。管程将这些分散在各进程中的临界区集中起来,并加以控制和管理,管程一次只允许一个进程进入.管程内,从而既便于系统管理共享资源,又能保证互斥。
信号量机制中:编写程序困难,易出错。所以产生了进程同步工具——管程。管程的特性保证了进程互斥,无须程序员自己实现互斥,从而降低了死锁发生的可能性,同时管程提供了条件变量可以让程序员灵活地实现进程同步。
利用共享数据结构抽象地表示系统中的共享资源,而把对该数据结构实施的操作定义为一组过程。进程对共享资源的申请、释放等操作,都通过这组过程来实现,这组过程还可以根据资源情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥。这个代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,称为“管程”(monitor)。
管程由4部分组成:
- 管程的名称
- 局部于管程内部的共享数据结构说明。
- 对该数据结构进行操作的一组过程(或函数)。
- 对局部于管程内部的共享数据设置初始值的语句。
描述举例如下:
monistor Demo{//1,定义一个名称为Demo的管程
// 2. 定义共享数据结构,对应系统中的某种共享资源
共享数据结构S;
// 4. 对共享数据结构初始化的语句
init_code(){
S=5;//初始资源为5
}
//3. 过程 1
take_away(){
对共享数据x的一系列处理;
S--;//可用资源减一
...;
}
// 过程2 :归还一个资源
give_back(){
对共享数据结构x的一系列处理;
S++;//可用的资源+1
}
}
- 管程把对共享资源的操作封装起来
- 每次仅允许一个进程进入管程,从而实现进程互斥
条件变量
当一个进程进入管程后被阻塞,且到阻塞的原因解除时,在此期间,如果该进程不释放管程,那么其他进程无法进入管程。为此,将阻基原因定义为条件变量condition
通常,**一个进程被阻塞的原因可以有多个,因此在管程中设置了多个条件变量。**每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程,对条件变量只能进行两种操作
即wait和 signal。
x.wait:当x对应的条件不满足时,正在调用**管程的进程调用x.wait将自己插入x条件的等待队列,并释放管程。**此时其他进程可以使用该管程。
x.signal: x对应的条件发生了变化,则调用x.signal,唤醒一个因x条件而阻塞的进程。
条件变量和信号变量的比较
相似点:条件变量的wait/signal操作类似于信号量的P/V操作,可以实现进程的阻塞唤醒。
不同点:
- 条件变量是没有值的,仅实现了”排队等待“功能;而信号量是”有值“的。
- 信号量的值反映了剩余资源数,在管程中,剩余资源数用共享数据结构记录。
错题
2.3.6 经典同步问题
1.生产者消费者问题
问题描述
一组生产者进程和一组消费者进程共享一个初始为空、大小为 n 的缓冲区,只有缓冲区没满时,生产者才把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中读取消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或一个消费者从中取出消息。
分析
- 关系分析:
生产者和消费者对缓冲区的访问属于互斥关系,而针对**“消息”则生产者和消费者属于协作关系**,只有生产者生产了消息,消费者才能使用消息,因此又是同步关系。 - 思路整理:
两组进程存在互斥和同步关系,也就是要解决互斥和同步PV的操作的位置。 - 信号量设置:
设置一个 mutex 为互斥信号量,用于控制互斥访问缓冲池,初值设为 1;信号量 full 用于记录当前缓冲池中的“满”缓冲区数,初值为 0;信号量 empty 用于记录当前缓冲池中“空”的缓冲区数,初值为 n;
进程描述如下:
seamphore mutex=1; //临界区互斥信号量
seamphore empty=n; //空闲缓冲区
seamphore full=0; // 缓冲区初始化为空
producer(){ // 生产者进程
while(1){
produce an item in nextp; // 生产数据
P(empty);(用什么,p一下)// 获取空缓冲区单元
P(mutex);(互斥夹紧) // 进入临界区
add nextp to buffer; (行为)//将数据放入缓冲区
V(mutex);(互斥夹紧) // 离开临界区,释放互斥信号量
V(full);(提供什么,V一下)//满缓冲区数加1
}
}
consumer(){ //消费者进程
while(1){
P(full); //获取满缓冲区单元
P(mutex); //进入临界区
remove an item from buffer; // 从缓冲区取出数据
V(mutex); // 离开临界区,释放互斥信号量
V(empty); // 空缓冲区数加 1
consume the item; //消费数据
}
}
注意:
-
加锁信号量的顺序不能打乱,否则容易出现死锁
首先P获取资源,最后临界区的前一行使用P(mutex)对临界区的访问互斥,如果P(mutex)在申请资源的前面,可能会导致死锁。
-
释放资源的顺序没有严格的要求。
多生产者和多消费者问题
问题描述
桌子上有一个盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈才可以向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。
分析
- 关系分析
爸爸和妈妈是互斥关系,爸爸和女儿、妈妈和儿子是同步关系,而且这两对进程必须连起来,儿子和女儿之间没有互斥和同步关系,因为他们是选择条件执行。 - 思路整理
这里一共由4个进程,可抽象为两个生产者和两个消费者被连接到大小为1的缓冲区上。 - 信号设置
信号量 plate 表示互斥信号量,用于确定是否可以往盘子中放水果,初值为 1 表示允许放入一个;信号量 apple 表示盘中是否还有苹果,初值为 0表示没有不许取;orange 表示盘中是否有橘子,初值同样为 0,orange=1 表示盘子中由橘子允许取。
代码如下:
semapore plate=1,apple=0,orange=0;
dad(){
while(1){
prepare an apple;
P(plate); //互斥向盘中取、放水果
put the apple on the plate; //向盘中放苹果
V(apple); // 允许取苹果
}
}
mom(){
while(1){
prepare an orange;
P(plate);
put the orange on the plate;
V(orange);
}
}
son(){
while(1){
P(orange); //互斥从盘中取橘子
take an orange from the plate;
V(plate); //允许向盘中放、取水果
eat the orange;
}
}
daughter(){
while(1){
P(apple);
take an aplle from the plate;
V(plate);
eat the apple;
}
}
注意:
因为盘子的数量为1 ,故去除对盘子的互斥之后依然可以互斥访问(不绝对)。盘子的容量大于1,必须要设置互斥信号量。
提供思路:
遇到类似生产者消费者问题时,仿照处理 同步与互斥的关系
2.读者写者问题
问题描述:
有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程同时访问共享数据时则可能导致数据不一致的错误,因此:
① 允许多个读者可以同时对文件执行读操作;
② 只允许一个写者往文件中写信息;
③ 任一写者在完成写操作之前不允许其他读者进程或写者工作;
④ 写者执行写操作前,应让已有的读者和写者全部退出。
分析
- 关系分析,读者和写者互斥的,写者和写者互斥,读者和读者之间不互斥。
- 思路整理:两个进程,读者和写者。由于写者和其他进程都互斥,因此可用互斥信号量的P操作、V操作解决;读者则较为复杂,与写者互斥的同时,又要与其他读者同步,需要一个计数器用于判断当前是否有读者读文件:当有读者时,写者不能写文件,此时读者一直占用文件直到退出,写者才可以写文件;同时,不同读者对计数器的访问也是互斥的。
- 信号量设置:设置 count 信号量为计数器,初值为 0;mutex 为互斥信号量,用于保护更新 count 变量时的互斥;互斥信号量 rw 用于保证读者和写者互斥访问。
第一种算法
int count=0;//记录当前读者的数量 (确保读者退出)
semaphore mutex=1;//用于保护更新count变量时的互斥(解决多读者访问)
semaphore rw=1;//用于保证读者和写者互斥地访问文件
writer(){
while(1){
P(rw); // 互斥访问共享文件
writing
V(rw); // 释放共享文件
}
}
reader(){
while(1){
P(mutex); // 互斥访问 count 变量
if(count==0) // 当第一个读进程读共享文件时
P(rw) // 阻止写进程
count++;
V(mutex); // 释放互斥变量 count
reading;
P(mutex);
count--;
if(count==0) // 当最后一个读进程读完共享文件
V(rw); // 允许写进程写
V(mutex);
}
}
此种方式下,可能导致写进程长时间等待甚至出现“饿死”的情况。改变上面这种读进程优先,让写进程优先,需要再增加一个信号量,并在上面的 writer() 和 reader() 函数中各增加一对PV操作
第二种算法(读写公平法)
int count=0; //记录当前读者的数量 (确保读者退出)
semaphore mutex=1; //用于保护更新count变量时的互斥(解决多读者访问)
semaphore rw=1; //用于保证读者和写者互斥地访问文件
semaphore w=1; // 实现写者优先(解决写进程饥饿问题)
writer(){
while(1){
P(w); // 在无写进程请求时进入
P(rw); // 互斥访问共享文件
writing
V(rw); // 释放共享文件
V(w); // 恢复对共享文件的访问
}
}
reader(){
while(1){
P(w);
P(mutex); // 互斥访问 count 变量
if(count==0) // 当第一个读进程读共享文件时
P(rw) // 阻止写进程
count++;
V(mutex); // 释放互斥变量 count
V(w);
reading;
P(mutex);
count--;
if(count==0) // 当最后一个读进程读完共享文件
V(rw); // 允许写进程写
V(mutex);
}
}
提供思路:
这里 对count变量的检查与赋值不能一气呵成导致一些错误,如果需要“一气呵成”自然想到使用 互斥信号量
这里解决 读读不互斥的问题,
同时提供一种类似多个读者的互斥的解决方案,即使用count进行解决,
第一个进程:进行加锁
最后一个进程:解锁
算法给出条件:
if(count==0)
3.哲学家进餐问题
问题描述
一张圆桌上坐着5名哲学家,每两名哲学家之间的桌子上摆着一根筷子,两根筷子之间是一碗米饭。哲学家倾注毕生精力于思考和进餐,哲学家思考时不影响其他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子——一根一根地拿起。若筷子已在他人手上,则需要等待。饥饿地哲学家只有同时拿到了两根筷子才能开始进餐,进餐完毕,放下筷子继续思考。
分析
- 关系分析:5 名哲学家与左右邻座对其中间的筷子的访问时互斥关系。
- 思路整理:显而易见,5 个哲学家对应5 个进程,问题解决的关键就是如何让一名哲学家拿到左右两根筷子而不造成死锁或饥饿现象。
- 解决方法有两个:
- 一是让他们同时拿两根筷子;
- 二是对每名哲学家的动作制定规则,避免饥饿或死锁现象的发生。
- 解决方法有两个:
- 信号量设置:互斥信号量数组 chopstick[5]={1,1,1,1,1},用于对 5 个筷子的互斥访问;哲学家编号顺序:0~4,哲学家 I 左边筷子的编号为 i,哲学家右边筷子的编号为(i+1)%5。
第一种算法(同时拿两根筷子)
semaphore chopstick[5]={1,1,1,1,1};//定义信号量数组,并初始化i号哲学家的进程
Pi(){
do{
P(chopstick[i]); //取左边筷子
P(chopstick[(i+1)%5]);// 取右边筷子
eat;
V(chopstick[i]); //放回左边筷子
V(chopstick[(i+1)%5]);// 放回右边筷子
think;
}while(1);
}
此算法存在的问题就是,当5名哲学家都想要进餐并分别拿起左边的筷子时,所有的筷子将被拿光,等到他们再想拿起右边的筷子时,就会发生全被阻塞,出现死锁。
若要避免此种情况,可以增加限制条件(第二种方法),如:
- 至多允许4名哲学家同时进餐;
- 仅当一名哲学家左右两边筷子都可以用时,才允许他抓起筷子;
- 对哲学家顺序编号,奇数号哲学家先拿起左边筷子,然后拿起右边的,而偶数哲学家相反。
第二种算法(加限制条件)
semaphore chopstick[5]={1,1,1,1,1};//定义信号量数组,并初始化i号哲学家的进程
semaphore mutex=1;
Pi(){
do{
P(mutex); // 在取筷子前获得互斥量
P(chopstick[i]); //取左边筷子
P(chopstick[(i+1)%5]);// 取右边筷子
V(mutex); // 释放取筷子的信号量
eat;
V(chopstick[i]); //放回左边筷子
V(chopstick[(i+1)%5]);// 放回右边筷子
think;
}while(1);
}
提供思路:
当遇到类似哲学家问题时,首先可
定义信号量数组
增加限制条件,避免死锁问题
4.吸烟者问题
问题描述
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但要卷起一支烟,抽烟者需要三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种材料放到桌子上,循环反复如此。
分析
- 关系分析:供应者与三个抽烟者分别是同步关系。由于供应者无法同时满足两个或以上的抽烟者,三个抽烟者对抽烟这个动作互斥。
- 思路整理:显然有4个进程,供应者作为生产者向三个抽烟者提供材料。
- 信号量设置:信号量 offer1,offer2,offer3 分别表示烟草和纸组合的资源、烟草和胶水组合的资源、纸和胶水组合的资源,信号量 finish 用于互斥进行抽烟动作。
代码如下:
int random; // 存储随机数
semaphore offer1=0;
semaphore offer2=0;
semaphore offer3=0;
semaphore finish=0;
process P1(){
while(1){
random=a random num;
random=random%3;
if(random==0)
V(offer1); // 提供烟草和纸
else if(random==1)
V(offer2); // 提供烟草和胶水
else
V(offer3); //提供纸和胶水
put on ; // 将材料放在桌子上
P(finish);
}
}
process P2(){
while(1){
P(offer3);
working; // 拿起纸和胶水,卷成烟,抽掉
V(finish);
}
}
process P3(){
while(1){
P(offer2);
working; // 拿起烟草和胶水,卷成烟,抽掉
V(finish);
}
}
process P4(){
while(1){
P(offer1);
working; // 拿起纸和烟草,卷成烟,抽掉
V(finish);
}
}
提供思路:
可以生产多个产品的单生产者问题提供思路
“轮流让各个吸烟者吸烟”,用整形变量i实现“轮流过程”