0%

操作系统——进程同步——哲学家进餐问题

哲学家进餐问题

复习一下 pv操作 互斥锁

1
2
3
4
P --wait(信号量S){
S<=0
S-- //上锁
}
1
2
3
V --signal(信号量){
S++ //开锁
}

1679620287199

1
2
3
4
5
6
7
8
9
10
semaphpore chopstick[5]={1,1,1,1,1};  
// 所有信号量均被初始化为1,第i位哲学家的活动可描述为
do{
P(chopstick[i]); //取左筷子 当所有哲学家都拿起左筷子
P(chopstick[i+1]%5); //取右筷子 右筷子被阻碍
eat;
V(chopstick[i]); //放左筷子
V(chopstick[(i+1)%5]);//放右筷子
think;
}while(true)

方法一

至多只允许有4位哲学家同时去拿左边的筷子,仅当一名哲学家左右两边的筷子都可以使用时,才允许他抓筷子。

1
2
3
4
5
6
7
8
9
10
11
12
semaphpore chopstick[5]={1,1,1,1,1}; //筷子
semaphpore count=4; //控制最多只允许4名哲学家同时进餐
do{
P(count); //判断是否超过4个人进餐
P(chopstick[i]); //取左筷子
P(chopstick[i+1]%5); // 取右筷子
eating;
V(chopstick[i]); //放左筷子
V(chopstick[i+1]%5); //放右筷子
V(count); //用餐完毕
thinking;
}while(True);
1
2
3
4
5
6
A  count=3
B count=2
C count=1
D count=0
E (负数阻塞)
保证了四个同时进餐

方法二

对哲学家进行编号,

奇数哲学家,先拿左边筷子,再拿右边筷子

偶数哲学家,先拿右边筷子,再拿左边筷子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
semaphpore chopstick[5]={1,1,1,1,1}; //筷子
do{
if(i%2==1){//奇数
P(chopstick[i]); //取左筷子
P(chopstick[i+1]%5); // 取右筷子
}
else{ //偶数
P(chopstick[i]); //取左筷子
P(chopstick[i+1]%5); // 取右筷子
}
eating;
V(chopstick[i]); //放左筷子
V(chopstick[i+1]%5); //放右筷子
thinking;
}while(True)

作业

第1关:03哲学家进餐同步控制问题


任务描述

本关任务:编写程序实现哲学家进餐同步控制。

相关知识

为了完成本关任务,你需要掌握:

1.多个哲学家进餐同步模型与死锁的产生。

2.同步控制避免哲学家进餐产生死锁。

3.sem_timedwait函数的使用。

哲学家进餐问题

哲学家进餐问题是计算机操作系统中一个典型的同步问题,有五个哲学家在同一张圆桌,它们间隔放置了五只筷子,每个哲学家只有拿到两只筷子才可进餐,进餐完就进行思考。在该问题中筷子是一种临界资源,会被两个哲学家进行争用,在并发条件下,如果不进行适当控制,则线程发生死锁,可能产生所有哲学家无法进餐的情况。这样产生的死锁状态对计算机系统是不利的。 该线程运行模型如图: 哲学家进餐模型

哲学家问题死锁模拟

原始的哲学家线程是无限等待的,这不利于在线测评(限定程序的运行时长),因此本实训限定五个哲学家线程持续固定长的时间(无论成功或失败)即退出运行。 在本项目中有两种情况:一是不进行同步控制的哲学家线程,每个哲学家线程如果不进行控制,则所有哲学家线程都无法进行,这就是发生了死锁。二是对哲学家线程执行同步控制,每个哲学家线程不能取比自己编号大的信号量,经过控制的哲学家线程则不会发生死锁。

为了区分两种模式的运行效果不同,哲学家线程运行时还有一个统计线程Reporter,用于统计已经成功执行的线程数目。该实训项目说明访问临界资源进行限制后各线程可以执行完毕。

两种情况要有相同的开局,哲学家线程获得信号量后都持有信号量资源一段时间(200MS),第一种情况是不控制线程直接再申请新资源,在1.5秒后进行完成情况统计。第二种情况是奇数编号的线程首先判断下一个编号资源是否可用,如果不可用,则马上释放已有资源。等100*编号(100MS或300MS)后再申请资源,经过控制的哲学家线程全部可以执行完成。

程序编程说明

本项目采用C语言进行编写,采用的是linux平台的gcc编译器,主线程先执行未受控制的哲学家线程,并输出完成统计。再执行受控制的哲学家线程,也输出完成统计。

两种情况中,主线程初始化信号量资源,启动五个哲学家线程,主线程ReporterPhi函数第一次启动五个哲学家线程(Philosopher),这五个哲学家线程在获得一个筷子后,都保持一段时间。然后试图再去获得新的筷子,发生死锁,造成所有线程都无法完成任务。五个线程结束后,统计成功的数目。

哲学家线程死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203

![交错获取筷子](/images/操作系统/9.png)

执行完毕的哲学家进程马上归还筷子资源, ![归还资源的哲学家线程](/images/操作系统/10.png)

这样就使线程间可以依次执行完毕。主线程最后统计成功的数目。如果哲学家线程发生了死锁,则统计数会是0。经过同步控制的哲学家线程具有相互协调资源的能力,而不发生死锁,统计数会是5。

这里对信号量的操作使用的是```sem_timedwait```函数,它在一个规定的时间尝试获取信号量,如果信号量值是可用的(大于0),则该函数马上返回0值,如果信号量值是不可用的(不大于0),则该方法会等待指定的时长,在阻塞指定时长后返回值是-1。```sem_timedwait```函数使用的是绝时间值。

```sem_getvalue(&sem, &semvalue)```;该方法直接查看信号量的值。```sem_post(&sem)```方法增加信号量的值,相当于归还资源;```gettimeofday```方法获得机器当前时间绝对值。```time_add_ms```对指定时间变量增加给定的时长。

本实训核心内容是要求学生理解哲学家线程死锁的发生,以及如何避免发生死锁,要求学生对并发线程有较深刻的理解,了解linux平台线程的运用和信号量的使用。

#### 测试说明

项目没有输入数据,输出数据是两种情况下线程成功运行的数目。

测试输入:无 预期输出:1,1(非真正结果)

------

开始你的任务吧,祝你成功!

```c++
//stu001.c 哲学家进餐问题,程序模板,由学生完成缺失代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

static sem_t sem_chops[5];//筷子信号量
static sem_t sem_stop;//该信号量用于等待。
//时间递增方法
void time_add_ms(struct timeval *time, uint ms)
{
time->tv_usec += ms * 1000; // 微秒 = 毫秒 * 1000
if(time->tv_usec >= 1000000) // 进位,1000 000 微秒 = 1 秒
{
time->tv_sec += time->tv_usec / 1000000;
time->tv_usec %= 1000000;
}
}
//100毫秒
#define TIMEOUT_FIRST 100
//200毫秒
#define TIMEOUT_HOLD 200
//200毫秒
#define TIMEOUT_PHIEND 200
//200毫秒
#define TIME_REPORT 200

struct timeval begintime; //获取的机器初始开始时间值
struct timespec first_time; //首次获取筷子信号量
struct timespec phi_holdtime; //保持一段时间
struct timespec report_time; //汇报线程完成情况时间
struct timespec phi_endtime; //哲学家线程结束绝对时间值
static int finish[5];

//哲学家线程没有进行有效同步控制
void* Philosopher(void *arg)
{
int iIndex=*((int*)arg);
int iRetWait1=-1,iRetWait2=-1;
//iRetWait1是获取筷子资源1的结果,成功返回0,失败返回-1

//iRetWait2是获取筷子资源2的结果,成功返回0,失败返回-1
//获取第一个筷子成功后还要保持一段时间,继续请求下一个新筷子
//begin ******哲学家线程获取筷子资源进餐

//筷子信号量 //首次获取筷子信号量
iRetWait1=sem_timedwait(sem_chops+iIndex,&first_time);//获取筷子时间结束
//该信号量用于等待 //保持一段时间
sem_timedwait(&sem_stop,&phi_holdtime);
//筷子信号量 //首次获取筷子信号量
iRetWait2=sem_timedwait(sem_chops+(iIndex+1)%5,&first_time); //获取筷子时间结束
//end
if((iRetWait1==0)&&(iRetWait2==0)){
//检查获得两个筷子资源结果,都成功则为就餐成功
//设置任务完成标志为1,线程结束
finish[iIndex]=1;
//释放筷子资源iIndex
sem_post(&sem_chops[iIndex]);
//释放筷子资源
sem_post(&sem_chops[(iIndex+1)%5]);
}


//线程结束时间点:约为700MS
return;
}

//哲学家线程进行有效同步控制
void* PhilosopherGood(void *arg)
{
int iIndex=*((int*)arg);
int iRetWait1=-1,iRetWait2=-1;
int i=0;
/*begin ******哲学家线程获取筷子资源进餐-受同步控制*********************
//根据线程编号选择筷子编号并尝试获取第一个筷子,未成功获取则等待一段时间后重试
//成功后继续请求下一个新筷子
****end*****************************************/
for(i=0;(i<5)&&(iRetWait1!=0);i++){
//获取第一根筷子
if(iIndex%2==1){
iRetWait1=sem_timedwait(sem_chops+iIndex,&first_time);
}
iRetWait2=sem_timedwait(sem_chops+(iIndex+1)%5,&first_time);
//设置任务完成标志为1,线程结束
finish[iIndex]=1;
//释放筷子资源iIndex
sem_post(&sem_chops[iIndex]);
//释放筷子资源
sem_post(&sem_chops[(iIndex+1)%5]);
}
//线程结束时间点:约为700MS
return;
}


//该线程用于检测哲学家未经同步控制完成任务的数量
int ReporterPhi(void*(*phiFunc)(void*))
{
int iFinishCount=0;
pthread_t pids[5];
int arg[5];
int iRetWait1=-1,iRetWait2=-1,retVal=-1;
int i=0;
gettimeofday(&begintime, NULL);
//第一次获取筷子资源时间点:100MS
time_add_ms(&begintime, TIMEOUT_FIRST);
first_time.tv_sec = begintime.tv_sec;
first_time.tv_nsec = begintime.tv_usec * 1000;

//保持获取筷子资源到时间点:300MS
time_add_ms(&begintime, TIMEOUT_HOLD);
phi_holdtime.tv_sec = begintime.tv_sec;
phi_holdtime.tv_nsec = begintime.tv_usec * 1000;

//哲学家线程结束时间点:500毫秒
time_add_ms(&begintime, TIMEOUT_PHIEND);
phi_endtime.tv_sec= begintime.tv_sec;
phi_endtime.tv_nsec= begintime.tv_usec * 1000;

//汇报线程完成情况时间点:700MS
time_add_ms(&begintime, TIME_REPORT);
report_time.tv_sec = begintime.tv_sec;
report_time.tv_nsec = begintime.tv_usec * 1000;



sem_init(&sem_stop,0,0);//该信号量初值为0

//初始化完成数组为0,筷子信号量值为1
for(i=0;i<5;i++)
{
finish[i]=0;
//设置信量初值为1
sem_init(sem_chops+i,0,1);
}

for(i=0;i<5;i++)
{
arg[i]=i;//传给线程的参数值,用来区分线程实体
//创建消费者线程,pids+i是线程ID的保存地址,arg+i是线程的参数指针
pthread_create(pids+i,NULL,phiFunc,(void *)(&arg[i]));
}

//等待哲学家线程结束,但都没有完成任务。
for(i=0;i<5;i++)
{
pthread_join(pids[i],NULL); //主线程等待生产者线程结束
}
//等待到报告时间点:700MS
retVal = sem_timedwait(&sem_stop, &report_time);
iFinishCount=0;
for(i=0;i<5;i++)
{
if(finish[i]==1)
{iFinishCount++;}
}
return iFinishCount;
}



int main(int argc,char * argv[])
{
int i=0;
int phi1=-1,phi2=-1;
//调用无同步控制的哲学家线程
phi1=ReporterPhi(Philosopher);
//调用有同步控制的哲学家线程
phi2=ReporterPhi(PhilosopherGood);
//输出两种情况下完成任务的线程数
printf("%d,%d",phi1,phi2);
//销毁信号量资源
for(i=0;i<5;i++)
{
sem_destroy(sem_chops+i);
}
sem_destroy(&sem_stop);
return 0;
}
-------------本文结束感谢您的阅读-------------
老板你好,讨口饭吃