文章目录:
问题描述及分析
哲学家就餐问题规定了有5位哲学家正在进行思考和就餐两种活动。用餐在一个桌子上进行,桌子上面有5个盘子和5个叉子,按照循环的方式分配。
问题的约束条件:
- 每个哲学家需要两支筷子才能就餐;
- 每个哲学家可以从他的左边或者右边拿起筷子,但是一次只能拿一支;
- 哲学家只有在拿到两支筷子时才能就餐。我们必须设计一个协议,即事前和事后协议,确保哲学家只有拿到两支筷子才能够就餐;
- 每支筷子可以是拿起和放下的。
解决哲学家就餐问题的方案需要满足以下条件:
互斥原则
:每个哲学家在任何时刻只能与左右两边的筷子之一进行交互,即一次只能拿起一支筷子。无饥饿问题
:哲学家在尝试获取筷子时,若那支筷子已经被其它哲学家持有,则需要等待该筷子可以使用。需要避免某个哲学家等待长时间无法获取筷子。避免死锁
:死锁指每个哲学家都在等待筷子,导致无法继续就餐的情况。合理利用时间
:合理利用时间,使得每个哲学家都能够在适当的时间获得就餐的机会,而不是长期等待或浪费时间。
一次错误的尝试
下列代码使用信号量来实现临界区的互斥访问,通过循环让哲学家不停的思考和进餐。semaphore fork[5] = {1, 1, 1, 1, 1};
初始化了一个包含5个元素的信号量数组,初始值为1,表示该筷子可用。
如下所示,给出伪代码:
cpp
semaphore fork[5] = {1, 1, 1, 1, 1};
void philosopher(int i)
{
do
{
// thinking...
wait(fork[i]); // 等待左边的筷子
wait(fork[(i + 1) % 5]); // 等待右边的筷子
// eat
signal(fork[i]); // 放下左边的筷子
signal(fork[(i + 1) % 5]); // 放下右边的筷子
} while (true); // 无限循环,不断进行思考和进餐
}
该解决方案存在的问题:
该解决方案可能在某种交叉执行的情况下导致死锁,即当所有哲学家在尝试拿起右边的筷子之前都先拿起了左边的筷子时,就会发生死锁。在这种情况下,所有的哲学家都在等待右边的筷子而阻塞,但没有一个人会执行一条指令。
解决方案一
这种解决方案是只允许有四位哲学家同时拿起左边的筷子,我们可以增加一个信号量来实现,通过该信号量来限制哲学家并发进程的数量。
cpp
semaphore fork[5] = {1, 1, 1, 1, 1}; // 初始化叉子的信号量,初始值为1表示叉子可用
semaphore count = 4; // 控制最多允许四位哲学家同时进餐的信号量
void philosopher(int i)
{
do
{
// thinking...
wait(count); // 判断是否超过四人准备进餐,若超过则等待
wait(fork[i]); // 等待左边的叉子可用
wait(fork[(i + 1) % 5]); // 等待右边的叉子可用
// eat...
signal(fork[i]); // 释放左边的叉子,使其可用
signal(fork[(i + 1) % 5]); // 释放右边的叉子,使其可用
signal(count); // 用餐完毕,别的哲学家可以开始进餐
} while (true); // 循环,使哲学家不断进行思考和进餐的过程
}
该算法通过使用信号量来控制叉子的获取和释放,避免了死锁的发生。每个哲学家在进餐前会先判断是否超过最大允许同时进餐的数量,如果超过则需要等待。通过这种方式,保证了最多只有四位哲学家同时进餐。在每次进餐时,哲学家会依次获取左边和右边的叉子,进餐完毕后释放叉子,使其可供其他哲学家使用。该算法保证哲学家之间的竞争是有序的,避免了死锁的发生。
但是,这种算法依旧存在一些问题。在某些情况下,可能回出现饥饿问题,即某个哲学家一直无法获取两个筷子而无法就餐。
解决方案二
接下来尝试使用非对称算法,其中第五位哲学家与前四位哲学家的行为不同。该解决方案使用了信号量(semaphore)和监视器(monitor)两种方式来实现。
为每支筷子初始化一个信号量数组 fork,初始化值为1,表示筷子可用。
cpp
semaphore fork[5] = {1, 1, 1, 1, 1};
对于前四位哲学家:
cpp
semaphore fork[5] = {1, 1, 1, 1, 1};
void philosopher(int i)
{
do
{
// thinking...
wait(fork[i]);
wait(fork[(i + 1) % 5]);
// eat
signal(fork[i]);
signal(fork[(i + 1) % 5]);
} while (true);
}
对于第五位哲学家:
cpp
semaphore fork[5] = {1, 1, 1, 1, 1};
void philosopher(int i)
{
do
{
// thinking...
wait(fork[0]); // 等待右边的筷子可用
wait(fork[4]); // 等待左边的筷子可用
// eat
signal(fork[0]); // 释放右边的筷子
signal(fork[4]); // 释放左边的筷子
} while (true);
}
该方案有以下优势:
- 允许较大程度的并发性;
- 避免饥饿;
- 避免死锁;
- 更灵活;
- 公平性;
- 有界性;
算法伪代码:这段代码使用了监视器ForkMonitor来管理筷子的状态和哲学家的行为。fork数组记录了每个筷子的可用数量,OKtoEat条件变量数组用于等待哲学家能够进餐的条件。
takeForks操作用于哲学家获取筷子。如果左右筷子中有一个不可用,哲学家会等待相应的条件变量。一旦筷子都可用,哲学家将获取左右筷子。
releaseForks操作用于哲学家释放筷子。哲学家将释放左右筷子,并检查相邻筷子是否可用。如果相邻筷子可用,则发出相应的条件变量信号,以通知等待的哲学家。
cpp
monitor ForkMonitor:
integer array[0..4] fork ← [2, 2, 2, 2, 2]
condition array[0..4] OKtoEat
operation takeForks(integer i):
// 如果左右叉子有一个不可用,则等待
if fork[i] != 2:
waitC(OKtoEat[i])
// 获取左右叉子
fork[i + 1] ← fork[i + 1] - 1
fork[i - 1] ← fork[i - 1] - 1
operation releaseForks(integer i):
// 释放左右叉子
fork[i + 1] ← fork[i + 1] + 1
fork[i - 1] ← fork[i - 1] + 1
// 如果相邻叉子可用,则发出信号
if fork[i + 1] == 2:
signalC(OKtoEat[i + 1])
if fork[i - 1] == 2:
signalC(OKtoEat[i - 1])
下面是 c++ 中使用监视器实现的哲学家就餐问题代码(基于Linux系统):
- 下列实现能获得最大的并行度。其中使用一个数组
state_
来跟踪一个哲学家的状态(思考、吃饭、试图拿筷子(饥饿))。 - 一个哲学家只有在左右两个邻居哲学家都没有进餐的情况下才能进入进餐状态。第
i
位哲学家的邻居由宏LEFT
和RIGHT
定义。 - 该方案使用了一个信号量数组,每个信号量分别对应一个哲学家。当所需的筷子被占用时,想进餐的哲学家可以阻塞。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 哲学家数量
#define PNUM 5
// 哲学家的三种状态
#define THINKING 2
#define STARVATION 1
#define EATING 0
// 根据哲学家索引计算其左右哲学家索引
#define LEFT (phnum + 4) % PNUM
#define RIGHT (phnum + 1) % PNUM
int index[PNUM]; // 存储哲学家的索引
int times = 200; // 哲学家就餐的次数
// 定义一个监视器类
class Monitor
{
public:
Monitor()
{
for (int i = 0; i < PNUM; i++)
{
state_[i] = THINKING;
pthread_cond_init(&phCond_[i], nullptr);
}
pthread_mutex_init(&mutex_, nullptr);
}
~Monitor()
{
for (int i = 0; i < PNUM; i++)
pthread_cond_destroy(&phCond_[i]);
pthread_mutex_destroy(&mutex_);
}
// 检查是否满足就餐条件
void test(int phnum)
{
if (state_[(phnum + 1) % PNUM] != EATING && state_[(phnum + PNUM - 1) % PNUM] != EATING && state_[phnum] == STARVATION)
{
state_[phnum] = EATING;
pthread_cond_signal(&phCond_[phnum]);
}
}
// 哲学家拿筷子
void takeFork(int phnum)
{
pthread_mutex_lock(&mutex_);
state_[phnum] = STARVATION;
// 检查条件是否满足,不满足则等待
test(phnum);
if (state_[phnum] != EATING)
pthread_cond_wait(&phCond_[phnum], &mutex_);
cout << "Philosopher " << phnum + 1 << " is Eating." << endl;
pthread_mutex_unlock(&mutex_);
}
// 哲学家放下筷子
void putFork(int phnum)
{
pthread_mutex_lock(&mutex_);
state_[phnum] = THINKING;
// 检查旁边哲学家是否可以就餐
test(LEFT);
test(RIGHT);
pthread_mutex_unlock(&mutex_);
}
private:
int state_[PNUM]; // 哲学家状态
pthread_cond_t phCond_[PNUM]; // 条件变量
pthread_mutex_t mutex_; // 互斥锁
};
Monitor philObject;
void *philosopher(void *args)
{
int cnt = 0;
while (cnt < times)
{
int i = *(int *)args;
sleep(1);
philObject.takeFork(i);
sleep(0.5);
philObject.putFork(i);
cnt++;
}
return nullptr;
}
int main()
{
pthread_t threadID[PNUM];
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for (int i = 0; i < PNUM; i++)
index[i] = i;
for (int i = 0; i < PNUM; i++)
{
pthread_create(&threadID[i], &attr, philosopher, &index[i]);
cout << "Philosopher " << i + 1 << "is thinking..." << endl;
}
for (int i = 0; i < PNUM; i++)
pthread_join(threadID[i], nullptr);
pthread_attr_destroy(&attr);
pthread_exit(nullptr);
return 0;
}
运行结果: