1、生产者-消费者问题
1.1 问题描述
一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放进缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或一个消费者从中取出消息。
1.2 问题分析
(1)关系分析
生产者和消费者对缓冲区的访问是互斥关系,生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,它们是同步关系。
(2)整理思路
生产者和消费者消费者两个京城,正好是存在互斥和同步关系
(3)信息量设置
信号量mutex作为互斥信号量,用于控制互斥访问缓冲池,初值为1。信号量full用于记录当前缓冲池中的“满”缓冲区数,初值为0。信号量empty用于记录当前缓冲区中“空”缓冲区数量,初值为n。
1.3 具体描述
semaphore mutex = 1; //临界区互斥信号量
semaphore empty = n; //空闲缓冲区
semaphore full = 0; //生产者进程
producer(){
while(true) { //生产者进程
生产一个产品;
P(empty);(要用什么,P一下) //获取空缓冲区单元
P(mutex);(互斥夹紧) //进入临界区,锁住互斥信号量
将数据放入临界区;
P(mutex);(互斥夹紧) //离开临界区,释放互斥信号量
V(full); (提供什么,V一下) //满缓冲区+1
}
}
comsumer(){ //消费者进程
while(true) {
P(full); //获取满缓冲区单元
P(mutex); //进入临界区
从缓冲区中取出数据;
V(empty); //空缓冲区数+1
消费产品;
}
}
注意
疑问:若生产者先执行P(mutex),然后执行P(empty),消费者先执行(mutex),然后执行P(full),这样可不可以。
答:不可以,这样会造成死锁。假如生产者进程已将缓冲区放满,消费者进程没有取产品,即empty = 0,当下次仍然是生产者进程运行时,它先执行P(mutex)封锁信号量,再执行P(empty)将被阻塞,希望消费者取出产品后将其唤醒。轮到消费者进程运行时,它先执行P(mutex),然而由于生产者进程已经封锁mutex信号量,消费者进程也会被阻塞,这样一来生产者和消费者进程都将被阻塞,都指望对方唤醒自己,因此陷入了无休止的等待,造成死锁。同理如果消费者进程已将缓冲区取空,即full = 0,若下次还是消费者先运行,也会出现类似的死锁。不过生产者和消费者释放信号量时,mutex、full先释放哪一个无所谓。
2、读者-写者问题
2.1 问题描述
有读者和写者两组并发进程,共享一个文件。要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或者写者工作;④写者执行写操作前,应让已有的读者或者写者全部退出。
2.2 问题分析
(1)关系分析
有题目分析读者和写者是互斥的,写者和写者也是互斥的,而读者和读者不存在互斥问题。
(2)整理思路
两个进程,即读者和写者。写者是比较简单的,它和任何进程互斥,用互斥信号量的P操作、V操作即可解决。读者问题比较复杂,他必须在实现与写者互斥的同事,实现与其他读者的同步,因此简单的PV操作是无法解决问题的。这里用到一个计数器,初值为0,用它来判断当前是否有读者读文件。当有读者时,写者也无法写文件,此时读者会一直占用文件,当没有读者时,写者才可以写文件。同时,这里不同读者对计数器的访问也应该是互斥的。
(3)信号量设置
首先设置信号量count为计数器,用于记录当前读者的数量,初值为0;设置mutex为互斥型号量,用于保护更新count变量的互斥;设置互斥信号量rw,用于保证读者和写者的互斥访问。
2.3 具体描述
int count = 0; //用于记录当前的读者数量
semaphore mutex = 1; //用于保护更新count变量时的互斥
semaphore rw = 1; //用于保证读者和写者互斥访问文件
writer() {
while(true) {
P(rw); //互斥访问共享文件
写入;
V(rw); //释放共享文件
}
}
reader() {
while(true) {
P(mutex); //互斥访问count变量
if(count == 0) //当第一个读进程读共享文件时
P(rw); //阻止写进程写
count++; //读者计数器+1
V(mutex); //释放互斥变量count
读入;
P(mutex); //互斥访问count变量
count--; //读者计数器-1
if(count == 0) //当最后一个读进程读完共享文件
P(rw); //允许写进程写
V(mutex);
}
}
在上面的算法中,读进程是优先的,即当存在读进程时,写进程将被延迟,且只要有一个读进程活跃,随后而来的读进程都将被允许访问文件。这样的方式会导致写进程可能长时间等待,且存在写进程“饿死”的情况。
这里介绍一种读写公平的算法,即读写进程具有一样的优先级。
int count = 0; //用于记录当前的读者数量
semaphore mutex = 1; //用于保护更新count变量时的互斥
semaphore rw = 1; //用于保证读者和写者互斥访问文件
semaphore w = 1; //用于实现“读写公平”
writer() {
while(true) {
P(w); //在无写进程请求时进入
P(rw); //互斥访问共享文件
写入;
V(rw); //释放共享文件
V(w); //恢复对共享文件的访问
}
}
reader() {
while(true) {
P(w); //在无写进程请求时进入
P(mutex); //互斥访问count变量
if(count == 0) //当第一个读进程读共享文件时
P(rw); //阻止写进程写
count++; //读者计数器+1
V(mutex); //释放互斥变量count
V(w); //恢复对共享文件的访问
读入;
P(mutex); //互斥访问count变量
count--; //读者计数器-1
if(count == 0) //当最后一个读进程读完共享文件
P(rw); //允许写进程写
V(mutex);
}
}
当一个写进程访问文件时,若先有一些读进程要求访问文件,后有另一个写进程要求访问文件,则当前访问文件的进程结束对文件的写操作时,会是一个读进程而不是一个写进程占用文件(在信号量w的阻塞队列上,因为读进程先来,故排在阻塞队列队首,而V操作唤醒进程时唤醒的是队首进程,之后再来读进程会排在写进程后面,不会被一直阻塞,导致饿死)。
3、哲学家进餐问题
3.1 问题描述
一张圆桌边上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭,哲学家只能思考和进餐。哲学家思考的时候,并不影响他人,只有当哲学家饥饿时,才会试图拿起左右两根筷子(一根一根拿起)。若筷子已经在他人手上则需要等待。哲学家只有同时拿到两根筷子才可以开始进餐,进餐结束以后,放下筷子继续思考。(以下图为例子)
3.2 问题分析
(1)关系分析
5名哲学家与左右邻居对其中筷子的访问是互斥关系。
(2)整理思路
显然,这里有5个进程。本体的关键是如何让一名哲学家拿到左右两根筷子而不造成死锁或者饥饿现象。解决办法有两个:一是让他们同时拿两根筷子;而是对每名哲学家的动作指定规则,避免饥饿或者死锁现象的发生。
(3)信号量设置
定义互斥信号量数组chopstick[5] = {1,1,1,1,1},用于对5个筷子的互斥访问。哲学家按顺序编号为0~4,哲学家i左边筷子的编号为i,右边的筷子编号为(i+1)%5。
3.3 具体描述
semaphore chopstick[5] = {1,1,1,1,1}; //定义信号量数组chopstick[5],并初始化
Pi() { //i号哲学家的进程
do{
P(chopstick[i]); //取左边筷子
P(chopstick[i+1]%5); //取右边筷子
进餐;
V(chopstick[i]); //放回左边筷子
V(chopstick[i+1]%5); //放回右边筷子
思考;
}while(true);
}
该算法存在以下问题:当5名哲学家都想要进餐并分别拿起左边的筷子时,筷子已被拿光,等到他们再想拿起右后边的筷子时,就全部被阻塞,因此出现了死锁。
解决办法:为了防止死锁发生,可对哲学家进程施加一些限制条件,比如最多允许4名哲学家同时进餐;仅当一名哲学家左右筷子都可以用的时候,才允许他同时拿起筷子;对哲学家顺序编号,要求奇数号哲学家先拿起左边的筷子,然后再拿右边的筷子,偶数号的哲学家刚好相反。
若采用第二种方法:必须可以同时拿起左右两个筷子时,才能去抓起筷子。我们可以设置一个互斥变量,使得所有哲学家在拿筷子这一动作是互斥的。这样就不会出现一个哲学家拿起一个筷子以后,由于时间片消耗完,另一个哲学家进行拿起筷子这个动作,造成死锁。
```cpp
semaphore chopstick[5] = {1,1,1,1,1}; //定义信号量数组chopstick[5],并初始化
semaphore mutex = 1; //取筷子动作的互斥信号量
Pi() { //i号哲学家的进程
do{
P(mutex); //在取筷子前获得互斥量
P(chopstick[i]); //取左边筷子
P(chopstick[i+1]%5); //取右边筷子
V(mutex); //释放取筷子的信号量
进餐;
V(chopstick[i]); //放回左边筷子
V(chopstick[i+1]%5); //放回右边筷子
思考;
}while(true);
}
4、吸烟者问题
1.1 问题描述
假设一个系统有三个吸烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽调它,但是卷起并抽掉一支烟,抽烟者需要三种材料;烟草、纸和胶水。三个抽烟者中,第一个拥有烟草,第二个有用纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次随机将两种材料放在桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个型号告诉已完成,此时供应者就会将另外两种材料放在桌上,如此重复。
1.2 问题分析
(1)关系分析
供应者与三个抽烟者之间是同步关系。由于供应者无法同时满足两个或以上的抽烟者,三个抽烟者对于抽烟这个动作互斥。
(2)整理思路
显然这里有4个进程。供应者作为生产者向三个抽烟者提供材料。
(3)信号量设置
信号量offer1、offer2、offer3分别表示烟草和纸组合的资源、烟草和胶水组合的、纸和胶水组合的资源。信号量finish用于互斥进行抽烟动作。
1.3 具体描述
int random; //存储随机数
semaphore offer1 = 0 //定义信号量对应烟草和纸组合的资源
semaphore offer2 = 0 //定义信号量对应烟草和胶水组合的资源
semaphore offer3 = 0 //定义信号量对应纸和胶水组合的资源
semaphore finish = 0 //定义信号量表示抽烟是否完成
producer() {
while(true){
random = 任意一个整数随机数;
random = random % 3;
if(random == 0)
V(offer1); //提供烟草和纸
else if(random == 1)
V(offer2); //提供烟草和胶水
else
V(offer3); //提供纸和胶水
任意两种材料放在桌子上;
P(finish);
}
}
smoker1() {
while(true) {
P(offer3);
拿纸和胶水,卷成烟,抽调;
V(finish);
}
}
smoker2() {
while(true) {
P(offer2);
拿烟草和胶水,卷成烟,抽调;
V(finish);
}
}
smoker3() {
while(true) {
P(offer1);
拿烟草和纸,卷成烟,抽调;
V(finish);
}
}