哲学家就餐问题

文章目录:

  • 问题描述及分析
  • 一次错误的尝试
  • 解决方案一
  • 解决方案二

问题描述及分析

哲学家就餐问题规定了有5位哲学家正在进行思考和就餐两种活动。用餐在一个桌子上进行,桌子上面有5个盘子和5个叉子,按照循环的方式分配。

在这里插入图片描述

问题的约束条件:

  • 每个哲学家需要两支筷子才能就餐;
  • 每个哲学家可以从他的左边或者右边拿起筷子,但是一次只能拿一支;
  • 哲学家只有在拿到两支筷子时才能就餐。我们必须设计一个协议,即事前和事后协议,确保哲学家只有拿到两支筷子才能够就餐;
  • 每支筷子可以是拿起和放下的。

解决哲学家就餐问题的方案需要满足以下条件:

  • 互斥原则:每个哲学家在任何时刻只能与左右两边的筷子之一进行交互,即一次只能拿起一支筷子。
  • 无饥饿问题:哲学家在尝试获取筷子时,若那支筷子已经被其它哲学家持有,则需要等待该筷子可以使用。需要避免某个哲学家等待长时间无法获取筷子。
  • 避免死锁:死锁指每个哲学家都在等待筷子,导致无法继续就餐的情况。
  • 合理利用时间:合理利用时间,使得每个哲学家都能够在适当的时间获得就餐的机会,而不是长期等待或浪费时间。

一次错误的尝试

下列代码使用信号量来实现临界区的互斥访问,通过循环让哲学家不停的思考和进餐。semaphore fork[5] = {1, 1, 1, 1, 1}; 初始化了一个包含5个元素的信号量数组,初始值为1,表示该筷子可用。

如下所示,给出伪代码:

semaphore fork[5] = {1, 1, 1, 1, 1};void philosopher(int i)
{do{// thinking...wait(fork[i]);           // 等待左边的筷子wait(fork[(i + 1) % 5]); // 等待右边的筷子// eatsignal(fork[i]);           // 放下左边的筷子signal(fork[(i + 1) % 5]); // 放下右边的筷子} while (true);                // 无限循环,不断进行思考和进餐
}

该解决方案存在的问题:

该解决方案可能在某种交叉执行的情况下导致死锁,即当所有哲学家在尝试拿起右边的筷子之前都先拿起了左边的筷子时,就会发生死锁。在这种情况下,所有的哲学家都在等待右边的筷子而阻塞,但没有一个人会执行一条指令。

解决方案一

这种解决方案是只允许有四位哲学家同时拿起左边的筷子,我们可以增加一个信号量来实现,通过该信号量来限制哲学家并发进程的数量。

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,表示筷子可用。

semaphore fork[5] = {1, 1, 1, 1, 1};

对于前四位哲学家:

semaphore fork[5] = {1, 1, 1, 1, 1};void philosopher(int i)
{do{// thinking...wait(fork[i]);wait(fork[(i + 1) % 5]);// eatsignal(fork[i]);signal(fork[(i + 1) % 5]);} while (true);
}

对于第五位哲学家:

semaphore fork[5] = {1, 1, 1, 1, 1};void philosopher(int i)
{do{// thinking...wait(fork[0]); // 等待右边的筷子可用wait(fork[4]); // 等待左边的筷子可用// eatsignal(fork[0]); // 释放右边的筷子signal(fork[4]); // 释放左边的筷子} while (true);
}

该方案有以下优势:

  • 允许较大程度的并发性;
  • 避免饥饿;
  • 避免死锁;
  • 更灵活;
  • 公平性;
  • 有界性;

算法伪代码:这段代码使用了监视器ForkMonitor来管理筷子的状态和哲学家的行为。fork数组记录了每个筷子的可用数量,OKtoEat条件变量数组用于等待哲学家能够进餐的条件。

takeForks操作用于哲学家获取筷子。如果左右筷子中有一个不可用,哲学家会等待相应的条件变量。一旦筷子都可用,哲学家将获取左右筷子。

releaseForks操作用于哲学家释放筷子。哲学家将释放左右筷子,并检查相邻筷子是否可用。如果相邻筷子可用,则发出相应的条件变量信号,以通知等待的哲学家。

monitor ForkMonitor:integer array[0..4] fork ← [2, 2, 2, 2, 2]condition array[0..4] OKtoEatoperation takeForks(integer i):// 如果左右叉子有一个不可用,则等待if fork[i] != 2:waitC(OKtoEat[i])// 获取左右叉子fork[i + 1] ← fork[i + 1] - 1fork[i - 1] ← fork[i - 1] - 1operation releaseForks(integer i):// 释放左右叉子fork[i + 1] ← fork[i + 1] + 1fork[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 位哲学家的邻居由宏 LEFTRIGHT 定义。
  • 该方案使用了一个信号量数组,每个信号量分别对应一个哲学家。当所需的筷子被占用时,想进餐的哲学家可以阻塞。
#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) % PNUMint 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;
}

运行结果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/218990.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux学习笔记之六(进程之间的管道通信和信号处理)

目录 1、管道通信1.1、无名管道1.1、有名管道 2、信号处理2.1、信号的种类和发送2.2、信号的接受和处理 1、管道通信 管道通信是一个设备中进程与进程之间通信的一种方式&#xff0c;分为无名管道和有名管道两种。前者只能用于有亲缘关系的进程之间的通信&#xff0c;如父子进…

扫描条形码到电脑:Barcode to pc 4.6.3 Crack

像专业人士一样使用条形码将条形码发送到 PC 排名第一的智能手机扫描应用程序 将条形码即时发送到计算机程序并自动执行任务的最简单方法 受到全球 500,000 多名用户的信赖 条形码到 PC&#xff1a;Wi-Fi 扫描仪应用程序&#xff0c;条码到 PC&#xff1a;适用于 Android 和 i…

WorkPlus即时通讯软件,以自主安全为底座,连接工作的一切

在当今竞争激烈的商业环境中&#xff0c;中大型企业对于移动办公平台的需求越来越迫切。在众多可选的平台中&#xff0c;WorkPlus凭借其高性价比和针对中大型企业的特色功能&#xff0c;成为了许多企业的首选。本文将为各位读者深度解析WorkPlus私有化部署的优势&#xff0c;带…

vue+elementui如何实现在表格中点击按钮预览图片?

效果图如上&#xff1a; 使用el-image-viewer 重点 &#xff1a; 引入 import ElImageViewer from "element-ui/packages/image/src/image-viewer"; <template><div class"preview-table"><el-table border :data"tableData" …

HashMap的实现原;HashMap的工作原理;HashMap存储结构; HashMap 构造函数

文章目录 说一下HashMap的实现原理(非常重要)①HashMap的工作原理HashMap存储结构常用的变量HashMap 构造函数tableSizeFor() put()方法详解hash()计算原理resize() 扩容机制get()方法为什么HashMap链表会形成死循环 HashMap是我们在工作中使用到存储数据特别频繁的数据结构&am…

java容器

cow容器 copy on write 又被成为写时复制(读写分离)容器, 原理就是: 如果向一个数组中添加元素的时候,会将原来的数组复制一份为新的数组,原来的数组不会动,负责读处理,然后在新的数组中进行添加操作,添加完后,将新数组的地址,赋值给原来数组的地址 这种设计的好处是什么呢?…

C语言进阶之笔试题详解(1)

引言&#xff1a; 对指针知识进行简单的回顾&#xff0c;然后再完成笔试题。 ✨ 猪巴戒&#xff1a;个人主页✨ 所属专栏&#xff1a;《C语言进阶》 &#x1f388;跟着猪巴戒&#xff0c;一起学习C语言&#x1f388; 目录 引言&#xff1a; 知识简单回顾 指针是什么 指针变…

JDK、JRE、JVM的特点和关联

Java 的三个重要的概念是 JDK&#xff08;Java Development Kit&#xff09;、JRE&#xff08;Java Runtime Environment&#xff09;和 JVM&#xff08;Java Virtual Machine&#xff09;。它们之间有着密切的关联&#xff0c;同时又有不同的职责和特点。 JDK&#xff08;Java…

【matlab程序】matlab画台风符号和实例应用

【matlab程序】matlab画台风符号和实例应用 没有看文献&#xff0c;不知道文献中的符号什么样子&#xff0c;据我理解为这样子的&#xff1a; 因此&#xff0c;按照自己的理解做了这期。 结果浏览&#xff1a; 台风符号一切可改&#xff0c;可细细改。可是我不发论文&#xf…

死锁是什么?死锁是如何产生的?如何破除死锁?

1. 死锁是什么 多个线程同时被阻塞&#xff0c;它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞&#xff0c;因此程序不可能正常终止。 2. 死锁的三种典型情况 一个线程, 一把锁, 是不可重入锁, 该线程针对这个锁连续加锁两次, 就会出现死锁. 两个线程…

SAP smartforms二维码输出

此方法需要SAP_BASIS版本在731以上 TCODE-SE73 选择’系统条形码’点击 ‘更改’ 按步骤创建一个系统条形码 Module Size 调节二维码的尺寸 进入smartforms 创建样式 填入条形码名称 创建一张表单测试二维码&#xff0c;填入创建好的样式 测试结果&#xff1a;

C++二分查找:统计点对的数目

本题其它解法 C双指针算法&#xff1a;统计点对的数目 本周推荐阅读 C二分算法&#xff1a;得到子序列的最少操作次数 本文涉及的基础知识点 二分查找算法合集 题目 给你一个无向图&#xff0c;无向图由整数 n &#xff0c;表示图中节点的数目&#xff0c;和 edges 组成…