问题描述
问题描述
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生活锁。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。
在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。
解法
1.服务生解法
graph TD
请求干饭人 --> 服务生
服务生 --> 可以 -->分配筷子 -->干饭完还筷子
服务生 --> 不可以 -->不给干饭筷子
#include <iostream>
#include <thread>
#include <cstdio>
#include <algorithm>
#include <semaphore.h>
#include <mutex>
#include <vector>
sem_t sem[5]; // 5个叉子
std::mutex mutex;
const int PEOPLE = 5; // 5 个人
std::vector<int> serverRequest(int index)
{
mutex.lock();
int leftIndex = (index - 1 + PEOPLE) % PEOPLE;
int selfVal, leftVal;
sem_getvalue(&sem[index], &selfVal);
if (selfVal == 0)
{
mutex.unlock();
return {};
}
else
{
sem_getvalue(&sem[leftIndex], &leftVal);
if (leftVal == 1)
{
sem_wait(&sem[index]);
sem_wait(&sem[leftIndex]);
mutex.unlock();
return {index, leftIndex};
}
else
{
mutex.unlock();
return {};
}
}
}
void run(int index, int eatTime)
{
while (true)
{
std::vector<int> arr = serverRequest(index);
std::this_thread::sleep_for(std::chrono::milliseconds(100 * eatTime));
if (arr.empty())
continue;
else
{
// eat
std::cout <<index<<" eat :";
std::this_thread::sleep_for(std::chrono::milliseconds(100 * eatTime));
for (auto i : arr)
std::cout << i << ' ';
std::cout << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100 * eatTime));
//放筷子
for (auto &index : arr)
{
sem_post(&sem[index]);
}
break;
}
}
}
int main()
{
srand((unsigned long long)time(nullptr));
for (int i = 0; i < PEOPLE; i++)
sem_init(&sem[i], 0, 1);
std::thread t[PEOPLE];
for (int i = 0; i < PEOPLE; i++)
{
t[i] = std::thread(run, i, rand()%10);
}
for (int i = 0; i < PEOPLE; i++)
{
t[i].join();
}
for (int i = 0; i < 5; i++)
sem_destroy(&sem[i]);
return 0;
}
资源分级解法
graph TD
定规则 --> 开搞就是
另一个简单的解法是为资源(这里是餐叉)分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放,而且保证不会有两个无关资源同时被同一项工作所需要。在哲学家就餐问题中,资源(餐叉)按照某种规则编号为1至5,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。在这种情况下,当四位哲学家同时拿起他们手边编号较低的餐叉时,只有编号最高的餐叉留在桌上,从而第五位哲学家就不能使用任何一只餐叉了。而且,只有一位哲学家能使用最高编号的餐叉,所以他能使用两只餐叉用餐。当他吃完后,他会先放下编号最高的餐叉,再放下编号较低的餐叉,从而让另一位哲学家拿起后边的这只开始吃东西。
尽管资源分级能避免死锁,但这种策略并不总是实用的,特别是当所需资源的列表并不是事先知道的时候。例如,假设一个工作单元拿着资源3和5,并决定需要资源2,则必须先要释放5,之后释放3,才能得到2,之后必须重新按顺序获取3和5。对需要访问大量数据库记录的计算机程序来说,如果需要先释放高编号的记录才能访问新的记录,那么运行效率就不会高,因此这种方法在这里并不实用。
这种方法经常是实际计算机科学问题中最实用的解法,通过为分级锁指定常量,强制获得锁的顺序,就可以解决这个问题。
Chandy/Misra解法
允许任意的用户(编号P1, ..., Pn)争用任意数量的资源。 与迪科斯彻的解法不同的是,这里编号可以是任意的。
graph TD
找干饭叉 --> 与他竞争的邻居
与他竞争的邻居-->筷子{筷子是否干净?}
筷子 -->|Yes|C[ok];
筷子 -->|No |E[end]
1984年,K. Mani Chandy和J. Misra提出了哲学家就餐问题的另一个解法,允许任意的用户(编号P1, ..., Pn)争用任意数量的资源。与迪科斯彻的解法不同的是,这里编号可以是任意的。
1.对每一对竞争一个资源的哲学家,新拿一个餐叉,给编号较低的哲学家。每只餐叉都是“干净的”或者“脏的”。最初,所有的餐叉都是脏的。
2.当一位哲学家要使用资源(也就是要吃东西)时,他必须从与他竞争的邻居那里得到。对每只他当前没有的餐叉,他都发送一个请求。
3.当拥有餐叉的哲学家收到请求时,如果餐叉是干净的,那么他继续留着,否则就擦干净并交出餐叉。
4.当某个哲学家吃东西后,他的餐叉就变脏了。如果另一个哲学家之前请求过其中的餐叉,那他就擦干净并交出餐叉。
这个解法允许很大的并行性,适用于任意大多问题。