在哲学家就餐问题中,假设有五位哲学家围坐在圆桌前,每位哲学家需要进行思考和进餐两种活动。他们的思考不需要任何资源,但进餐需要使用两根筷子(左右两侧各一根)。筷子是共享资源,哲学家们在进行进餐时需要竞争筷子,而且不能出现死锁情况,即每位哲学家都能在有可能的情况下进餐。
问题示例:
假设有五位哲学家,编号为 0 到 4,围坐在圆桌周围,每位哲学家面前有一根筷子。他们的行为可以描述如下:
-
思考:哲学家在没有筷子的时候,思考一段时间。
-
进餐:哲学家只有同时拿到左右两边的筷子才能进餐,进餐完成后放下筷子继续思考。
解决方案概述:
- 状态记录:每位哲学家有三种状态:思考、饥饿和进餐。
- 互斥保护:使用互斥锁保护对状态的访问,确保状态变化的原子性。
- 信号量:使用信号量控制每根筷子的使用,确保每位哲学家能同时拿到左右两根筷子。
我们用一个数组 state 来记录每一位哲学家的三个状态,分别是在进餐状态、思考状态、饥饿状态(正在试图拿叉子)。
那么,一个哲学家只有在两个邻居都没有进餐时,才可以进入进餐状态。
第 i
个哲学家的左邻右舍,则由宏 LEFT
和 RIGHT
定义:
- LEFT : ( i + 5 - 1 ) % 5
- RIGHT : ( i + 1 ) % 5
比如 i 为 2,则 LEFT
为 1,RIGHT
为 3。
解决步骤:
-
定义全局变量和初始化:
- 定义哲学家状态数组
state[]
,互斥锁pthread_mutex_t mutex
和信号量数组sem_t chopsticks[]
。 - 初始化互斥锁和每个筷子的信号量。
- 定义哲学家状态数组
-
哲学家线程函数设计:
- 哲学家线程循环执行思考和进餐过程。
- 在饥饿状态时,试图获取两侧筷子,获取成功则进餐,否则等待。
- 进餐后释放筷子,继续思考。
-
获取和释放筷子的操作:
- 使用互斥锁保护对状态数组的修改,确保线程安全。
- 利用信号量控制筷子的获取和释放,避免死锁和资源争夺。
示例代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define NUM_PHILOSOPHERS 5
#define THINKING 0
#define HUNGRY 1
#define EATING 2
int state[NUM_PHILOSOPHERS]; // 哲学家的状态数组
pthread_mutex_t mutex; // 互斥锁,保护对状态数组的访问
sem_t chopsticks[NUM_PHILOSOPHERS]; // 每根筷子的信号量数组
void *philosopher(void *arg) {
int id = *((int *)arg);
while (1) {
// 思考
printf("哲学家 %d 正在思考。\n", id);
sleep(rand() % 3 + 1); // 模拟思考时间
// 进入饥饿状态
printf("哲学家 %d 饿了。\n", id);
pthread_mutex_lock(&mutex);
state[id] = HUNGRY;
printf("哲学家 %d 尝试拿起筷子。\n", id);
test(id); // 尝试获取两侧的筷子
pthread_mutex_unlock(&mutex);
sem_wait(&chopsticks[id]); // 获取筷子,如果没有则阻塞
// 进餐
printf("哲学家 %d 开始进餐。\n", id);
sleep(rand() % 3 + 1); // 模拟进餐时间
// 放下筷子
sem_post(&chopsticks[id]); // 放下左侧筷子
sem_post(&chopsticks[(id + 1) % NUM_PHILOSOPHERS]); // 放下右侧筷子
printf("哲学家 %d 放下筷子,开始思考。\n", id);
}
pthread_exit(NULL);
}
void test(int id) {
if (state[id] == HUNGRY && state[(id + NUM_PHILOSOPHERS - 1) % NUM_PHILOSOPHERS] != EATING
&& state[(id + 1) % NUM_PHILOSOPHERS] != EATING) {
state[id] = EATING;
sem_post(&chopsticks[id]); // 左侧筷子
sem_post(&chopsticks[(id + 1) % NUM_PHILOSOPHERS]); // 右侧筷子
}
}
int main() {
pthread_t philosophers[NUM_PHILOSOPHERS];
int ids[NUM_PHILOSOPHERS];
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 初始化每根筷子的信号量
for (int i = 0; i < NUM_PHILOSOPHERS; ++i) {
sem_init(&chopsticks[i], 0, 1); // 初始值为1,表示筷子可用
}
// 创建哲学家线程
for (int i = 0; i < NUM_PHILOSOPHERS; ++i) {
ids[i] = i;
pthread_create(&philosophers[i], NULL, philosopher, (void *)&ids[i]);
}
// 等待线程结束
for (int i = 0; i < NUM_PHILOSOPHERS; ++i) {
pthread_join(philosophers[i], NULL);
}
// 清理资源
pthread_mutex_destroy(&mutex);
for (int i = 0; i < NUM_PHILOSOPHERS; ++i) {
sem_destroy(&chopsticks[i]);
}
return 0;
}
解释和步骤:
1. 全局变量和初始化
state[]
数组:每个元素表示一个哲学家的状态,初始为思考状态。pthread_mutex_t mutex
:互斥锁,保护对state[]
数组的访问。sem_t chopsticks[NUM_PHILOSOPHERS]
数组:每根筷子对应一个信号量,初始为可用状态。
2. 哲学家线程函数 philosopher
- 思考阶段:每个哲学家在思考一段时间后进入饥饿状态。
- 饥饿阶段:哲学家试图获取左右两根筷子,如果成功则进入进餐状态,否则等待。
- 进餐阶段:成功获取筷子后进行进餐,一段时间后释放筷子,回到思考阶段。
3. test
函数
- 检查能否进入进餐状态:检查当前哲学家及其左右邻居的状态,如果都是饥饿状态且两侧筷子可用,则将当前哲学家状态设置为进餐,并释放左右两根筷子。
4. 主函数 main
- 初始化:初始化互斥锁和每根筷子的信号量。
- 创建线程:创建并启动每个哲学家的线程。
- 等待线程结束:主线程等待所有哲学家线程执行完毕。
- 清理资源:销毁互斥锁和每根筷子的信号量。