哲学家就餐问题、银行家算法、读者写者问题、生产者消费者问题
哲学家就餐问题
该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
解题思路
因为是五位哲学家,并且每位哲学家的各自做自己的事情(思考和吃饭),因此可以创建五个线程表示五位哲学家,五个线程相互独立(异步)。并对五位哲学家分别编号为0~4。
同时,有五根筷子,每根筷子只对其相邻的两位哲学家是共享的,因此这五根筷子可以看做是五种不同的临界资源(不是一种资源有5个,因为每根筷子只能被固定编号的哲学家使用)。并对五根筷子分别编号为0~4,其中第i号哲学家左边的筷子编号为i,则其右边的筷子编号就应该为(i + 1) % 5。
因为筷子是临界资源,因此当一个线程在使用某根筷子的时候,应该给这根筷子加锁,使其不能被其他进程使用。
根据以上分析,可以使用pthread_create函数创建五个线程,可以使用pthread_mutex_t chops[5]表示有五根筷子,五个不同的临界资源,并用pthread_mutex_init(&chops[i], NULL);来初始化他们。
根据上面的分析,可以得到一个基本的解决方案如下:
void philosopher (void* arg) {
while (1) {
think();
hungry();
pthread_mutex_lock(&chopsticks[left]);
pthread_mutex_lock(&chopsticks[right]);
eat();
pthread_mutex_unlock(&chopsticks[left]);
pthread_mutex_unlock(&chopsticks[right]);
}
}
这段伪代码的思路很明确,这个函数代表的是一个哲学家的活动,可以将其创建为五个不同的线程代表五位不同的哲学家。每位哲学家先思考,当某位哲学家饥饿的时候,就拿起他左边的筷子,然后拿起他右边的筷子,然后进餐,然后放下他左右的筷子并进行思考。因为筷子是临界资源,所以当一位哲学家拿起他左右的筷子的时候,就会对他左右的筷子进行加锁,使其他的哲学家不能使用,当该哲学家进餐完毕后,放下了筷子,才对资源解锁,从而使其他的哲学家可以使用。
这个过程看似没什么问题,但是当你仔细分析之后,你会发现这里面有一个很严重的问题,就是死锁,就是每个线程都等待其他线程释放资源从而被唤醒,从而每个线程陷入了无限等待的状态。在哲学家就餐问题中,一种出现死锁的情况就是,假设一开始每位哲学家都拿起其左边的筷子,然后每位哲学家又都尝试去拿起其右边的筷子,这个时候由于每根筷子都已经被占用,因此每位哲学家都不能拿起其右边的筷子,只能等待筷子被其他哲学家释放。由此五个线程都等待被其他进程唤醒,因此就陷入了死锁。
问题解决
方法一
第一种解决死锁问题的办法就是同时只允许四位哲学家同时拿起同一边的筷子,这样就能保证一定会有一位哲学家能够拿起两根筷子完成进食并释放资源,供其他哲学家使用,从而实现永动,避免了死锁。举个最简单的栗子,假定0~3号哲学家已经拿起了他左边的筷子,然后当4号哲学家企图去拿他左边的筷子的时候,将该哲学家的线程锁住,使其拿不到其左边的筷子,然后其左边的筷子就可以被3号哲学家拿到,然后3号哲学家进餐,释放筷子,然后更多的哲学家拿到筷子并进餐。
如何才能实现当4号哲学家企图拿起其左边的筷子的时候将该哲学家的线程阻塞?这个时候就要用到该问题的提出者迪杰斯特拉(这货还提出了迪杰斯特拉最短路径算法,著名的银行家算法也是他发明的)提出的信号量机制。因为同时只允许有四位哲学家同时拿起左筷子,因此我们可以设置一个信号量r,使其初始值为4,然后每当一位哲学家企图去拿起他左边的筷子的时候,先对信号量做一次P操作,从而当第五位哲学家企图去拿做筷子的时候,对r做一次P操作,r = -1,由r < 0得第五位哲学家的线程被阻塞,从而不能拿起左筷子,因此也就避免了死锁问题。然后当哲学家放下他左边的筷子的时候,就对r做一次V操作。
void philosopher (void* arg) {
while (1) {
think();
hungry();
P(&r);//C语言提供的P操作的函数是sem_wait
pthread_mutex_lock(&chopsticks[left]);
pthread_mutex_lock(&chopsticks[right]);
eat();
pthread_mutex_unlock(&chopsticks[left]);
V(&r);//C语言提供的V操作的函数是sem_post
pthread_mutex_unlock(&chopsticks[right]);
}
}
方法二
第二种解决死锁问题的办法就是使用AND信号量机制,意思就是如果想给某个哲学家筷子,就将他需要的所有资源都给他,然后让他进餐,否则就一个都不给他。
根据上面的分析,可以将代码修改为:
void philosopher (void* arg) {
while (1) {
think();
hungry();
Swait(chopsticks[left], chopsticks[right]);
eat();
Spost(chopsticks[left], chopsticks[right]);
}
}
但是C语言的库里面并没有给提供AND型信号量,但是我们可以利用互斥量简单的替代一下AND信号量,就是设置一个全局互斥量mutex,用来锁住全部的临界资源,当一个哲学家企图拿筷子的时候,就将所有的资源锁住,然后让他去拿他需要的筷子,等他取到他需要的筷子之后,就解锁,然后让其他哲学家取筷子。代码如下:
void philosopher (void* arg) {
while (1) {
think();
hungry();
pthread_mutex_lock(mutex);
pthread_mutex_lock(&chopsticks[left]);
pthread_mutex_lock(&chopsticks[right]);
pthread_mutex_unlock(mutex);
eat();
pthread_mutex_unlock(&chopsticks[left]);
pthread_mutex_unlock(&chopsticks[right]);
}
}
方法三
第三种解决的办法就是规定奇数号哲学家先拿起他左边的筷子,然后再去拿他右边的筷子,而偶数号的哲学家则相反,这样的话总能保证一个哲学家能获得两根筷子完成进餐,从而释放其所占用的资源,代码如下:
void philosopher (void* arg) {
int i = *(int *)arg;
int left = i;
int right = (i + 1) % N;
while (1) {
printf("哲学家%d正在思考问题\n", i);
delay(50000);
printf("哲学家%d饿了\n", i);
if (i % 2 == 0) {//偶数哲学家,先右后左
pthread_mutex_lock(&chopsticks[right]);
pthread_mutex_lock(&chopsticks[left]);
eat();
pthread_mutex_unlock(&chopsticks[left]);
pthread_mutex_unlock(&chopsticks[right]);
} else {//奇数哲学家,先左后又
pthread_mutex_lock(&chopsticks[left]);
pthread_mutex_lock(&chopsticks[right]);
eat();
pthread_mutex_unlock(&chopsticks[right]);
pthread_mutex_unlock(&chopsticks[left]);
}
}
}
银行家算法
操作系统在解决死锁问题的时候,有四个大的方向,分别是预防死锁,避免死锁,检测死锁和解除死锁。而今天要讲的银行家算法是避免死锁算法的代表。
安全状态
避免死锁的定义:在资源动态分配的过程中,防止系统进入不安全状态。
要想实现避免死锁,那么就必须让进程资源分配处于安全状态,所谓安全状态就是系统能按照某种进程推进顺序(安全序列)进行。
我们先来说答案,其中按照 <P2,P1,P3>的推进序列使用磁带机就是安全的。
我们来看
- 现在的可用(可以分配)的资源数是 3 ,而 P2 需要的资源数是 2,所以可以分配
- 待 P2 执行完毕后,释放资源数为 4,变成可用资源,加上之前剩余的可用资源数,一共是 5 个
- 观察到 P1 需要 5 个,所以分配给 P1
- P1 执行完后释放资源数为 10,变成可用资源
- 而 P3 只需要 7 个,所以分配 7 个可用资源个 P3
- P3 运行完毕
并非所有的不安全状态都是死锁,但是处于不安全状态很可能产生死锁
银行家算法的数据结构
- 可利用资源向量(Available):系统还可以分配的资源
- 最大需求矩阵(Max):进程的最大资源需要
- 分配矩阵(Alloction):进程已经获得的资源
- 需求矩阵(Need):进程还需要获得的资源
银行家算法
- 假设 P1 进程提出请求 K 个资源
- 如果 K <= Need,就继续步骤;否则出错,因为请求资源 K 不能超过还需要获得的资源
- 如果 K <= Available,就继续步骤;否则出错,因为请求资源 K 不能超过系统还可以分配的资源 Available
- 系统试探分配资源,并修改下列数据 Available = Available - K;表示分配给 P1 K
个资源后,还剩多少系统可分配资源 Allocation = Allocation + K;表示 P1 已经获得的资源 Need =
Need - K;表示进程 P1 还需要获得的资源 - 此时系统执行安全性算法,计算进程是否处于安全性状态
安全性算法
安全性算法是银行家算法在第五步执行的子算法,用于检查进程的安全状态。银行家算法是计算某一个进程对资源的需求问题,安全性算法是计算所有的进程在各自的银行家算法执行下,是否处于安全状态。
两个向量
- 工作向量(Work):系统提供给进程的各类资源数目
- Finish:表示系统是否有足够的资源分配给进程,这是一个布尔值。初始化为 false。
算法描述
在进程集合中找到下述条件的进程
- Finish[ i ] = false;
- Need <= Work
- 进程执行完毕
- Work = Work + Allocation
- Finish [ i ] = true
- 返回继续执行 1 ,寻找其他的进程分配资源
- 若所有的 Finish 为 true 则安全
银行家算法实例
分析此时刻,P1 和 P3 都可以进行分配(PS:安全序列的结果并不是唯一的)
利用上述的银行家算法即可得出此时的安全序列:<P1,P3,P4,P2,P0>
读者写者问题
一个数据文件或记录可被多个进程共享。
- 只要求读文件的进程称为“Reader进程”,其它进程则称为“Writer进程”。
- 允许多个进程同时读一个共享对象,但不允许一个Writer进程和其他Reader进程或Writer进程同时访问共享对象
方法1:利用记录型信号量解决读者–写者问题
互斥信号量wmutex: 实现Reader与Writer进程间在读或写时的互斥,整型变量Readcount: 表示正在读的进程数目;
由于只要有一个Reader进程在读,便不允许Writer进程写。所以,仅当Readcount=0,即无Reader进程在读时,Reader才需要执行Wait(wmutex)操作。若Wait(wmutex)操作成功,Reader进程便可去读,相应地,做Readcount+1操作。
同理,仅当Reader进程在执行了Readcount减1操作后其值为0时,才需执行signal(wmutex)操作,以便让Write进程写
互斥信号量rmutex: Reader进程间互斥访问Readcount
方法2:利用信号量集解决读者–写者问题
增加一个限制:最多只允许RN个读者同时读
引入信号量L,并赋予其初值RN,通过执行Swait(L, 1, 1)操作,来控制读者的数目。
解释:
每当有一个读者进入时,就要先执行Swait(L, 1, 1)操作,使L的值减1。当有RN个读者进入读后,L便减为0,第RN +1个读者要进入读时,必然会因Swait(L, 1, 1)操作失败而阻塞。
般信号量集的几种特殊情况:
- Swait(S, d, d),只有一个信号量S,允许每次申请d个资源,若现有资源数少于d,不予分配。
- Swait(S, 1, 1),蜕化为一般的记录型信号量(S>1时)或互斥信号量(S=1时)。
- Swait(S, 1, 0),当S>=1时,允许多个进程进入某特定区,当S变为0后,阻止任何进程进入特定区,相当于可控开关。
生产者消费者问题
生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
- 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
- 如果共享数据区为空的话,阻塞消费者继续消费数据;
来源:CSDN
作者:q435201823
链接:https://blog.csdn.net/q435201823/article/details/103542909