一、实验题目
模拟实现读者-写者问题
二、实验目的
通过模拟实现经典的读者——写者问题,巩固对同步机制的学习,学会运用信号量、PV原语处理进程间的同步和互斥。
三、实验要求
设计实现读者-写者问题模拟程序。在Windows 环境下,创建一个控制台程序,此程序包含n个线程。用这n个线程来表示n个读者或写者。每个线程按相应测试数据文件(后面有介绍)的要求进行读写操作。用信号量机制分别实现读者优先或写者优先的读者-写者问题。
四、解决方案
进程在start变量代表的时间来临时进入就绪状态。此时尝试按照type来决定上读锁或是写锁,若上锁成功则调用run方法对time与ending进行更改。若上错不成功则下一秒继续尝试上锁。采用Class Process作为模拟进程对象,其中存放与进程运行相关的参数。
class Process {
//构造方法不在此赘述
int id;//进程编号
char type;//取值r或w,表示进程是读进程还是写进程
int start;//进程加入处理流程开始时间
int time;//进程运行完需要的时间
bool running;//此时进程是否在运行
} p[10];
同时我采用四个变量:
bool readLock = 0;(读锁)
bool writeLock = 0;(写锁)
int readCount = 0;(读进程计数)
int ending = 0;(结束的进程数)
表示当前对于模拟文件的读写互斥情况。以及四个方法:
bool addReadLock(Process p);(上读锁)
bool addWriteLock(Process p);(上写锁)
void removeReadLock (Process p);(解除读锁)
void removeWriteLock(Process p);(解除写锁)
使进程获得操作锁标记量的权利。
以下是程序设计中的处理流程图。
五、源程序
#include <iostream>
#include <deque>
using namespace std;
class Process {
public:
Process() {
}
~Process() {
}
Process(int id, char type, int start, int time) {
this->id = id;
this->type = type;
this->start = start;
this->time = time;
this->running = 0;
}
int id;
char type;
int start;
int time;
bool running;
} p[10];
bool readLock = 0;
bool writeLock = 0;
int readCount = 0;
int ending = 0;
bool addReadLock(Process p) {
if (writeLock) {
printf("进程%d上读锁失败\n", p.id);
return 0;
} else {
if (!readLock)
readLock = 1;
readCount++;
}
printf("进程%d上读锁成功\n", p.id);
return 1;
}
bool addWriteLock(Process p) {
if (readCount) {
printf("进程%d上写锁失败\n", p.id);
return 0;
} else {
writeLock = 1;
}
printf("进程%d上写锁成功\n", p.id);
return 1;
}
void removeReadLock(Process p) {
if (readCount == 1) {
readLock = 0;
}
readCount--;
printf("进程%d解除读锁\n", p.id);
}
void removeWriteLock(Process p) {
writeLock = 0;
printf("进程%d解除写锁\n", p.id);
}
void run(Process &p) {
printf("进程%d正在运行,剩余时间:%ds\n", p.id, p.time);
if (p.time == 1) {
ending++;
if (p.type == 'r') {
removeReadLock(p);
} else {
removeWriteLock(p);
}
p.running = 0;
}
p.time -= 1;
}
int main() {
deque<int> ready;
deque<int> endPrint;
p[1] = Process(1, 'w', 3, 5);
p[2] = Process(2, 'w', 16, 5);
p[3] = Process(3, 'r', 5, 2);
p[4] = Process(4, 'w', 6, 5);
p[5] = Process(5, 'r', 4, 3);
p[6] = Process(6, 'r', 11, 4);
int second = 0;
while (ending != 6 || !endPrint.empty()) {
printf("---------------------第%d秒---------------------\n", second);
for (int id : endPrint) {
printf("进程%d结束运行\n", id);
}
endPrint.clear();
//进程加入!
for (int i = 1; i <= 6; i++) {
if (second >= p[i].start && p[i].time > 0 && !p[i].running) {
ready.push_back(i);
printf("进程%d已经就绪\n", i);
}
}
//进程执行!
for (int i : ready) {
if (writeLock) {
break;
} else if (readCount) { //有读锁
if (p[i].type == 'r' && addReadLock(p[i])) {
p[i].running = 1;
}
} else { //没写锁没读锁。可以上锁
if (p[i].type == 'r') {
if (addReadLock(p[i]))
p[i].running = 1;
} else {
if (addWriteLock(p[i]))
p[i].running = 1;
}
}
}
for (int i = 1; i <= 6; i++) {
if (p[i].time == 1) {
endPrint.push_back(p[i].id);
}
if (p[i].running)
run(p[i]);
}
ready.clear();
second++;
// _sleep(233);
}
return 0;
}
六、运行结果与分析
运行截图如下:
分析:本程序未使用信号量机制与PV操作。转而使用锁变量与操作锁方法实现。
优势:本程序各变量与方法简单易懂。
缺点:1.程序采用对象数组方式直接在程序中声明,且其后多处处理逻辑均基于测试数据的进程个数,健壮性较低。2.程序缺少阻塞队列的实现,每一秒都必须遍历整个进程数组,查找处于就绪态的进程,加入就绪队列,并判断上锁情况将其running属性标记为1。随后再次遍历进程数组找到这些“正在运行”的进程调用run()方法将其“执行”1秒。时空开销较大。3.在run()方法中进程剩余1秒时就会判定为ending,随后移除该进程申请的锁。这一步会导致主函数内每一秒的打印信息末尾并不会出现“该进程结束”的信息。需要另一个endPrint队列再下一秒内专门打印该进程的结束信息,可见处理逻辑比较脆弱。
七、实验小结
本实验模拟了简单的读者-写者问题实现。在实际的读写者问题里数据量可能远远超过本次模拟的范畴。此次实验加深了我对读者-写者问题的理解,检验了我对数据结构与操作系统课程交叉的知识的掌握,对代码中的不足我会在下一次实验尽最大的努力去处理与解决。