【实验目的】
理解临界区和进程互斥的概念,掌握用信号量和PV操作实现进程互斥的方法。
【实验内容】
在windows或者linux环境下编写一个控制台应用程序,该程序运行时能创建N个线程,其中既有读者线程又有写者线程,它们按照事先设计好的测试数据进行读写操作。请用信号量和PV操作实现读者/写者问题。
读者/写者问题的描述如下:
有一个被许多进程共享的数据区,这个数据区可以是一个文件,或者主存的一块空间,甚至可以是一组处理器寄存器。有一些只读取这个数据区的进程(reader)和一些只往数据区中写数据的进程(writer)。以下假设共享数据区是文件。这些读者和写者对数据区的操作必须满足以下条件:读—读允许;读—写互斥;写—写互斥。这些条件具体来说就是:
(1)任意多的读进程可以同时读这个文件;
(2)一次只允许一个写进程往文件中写;
(3)如果一个写进程正在往文件中写,禁止任何读进程或写进程访问文件;
(4)写进程执行写操作前,应让已有的写者或读者全部退出。这说明当有读者在读文件时不允许写者写文件。
对于读者-写者问题,有三种解决方法:
1、读者优先
除了上述四个规则外,还增加读者优先的规定,当有读者在读文件时,对随后到达的读者和写者,要首先满足读者,阻塞写者。这说明只要有一个读者活跃,那么随后而来的读者都将被允许访问文件,从而导致写者长时间等待,甚至有可能出现写者被饿死的情况。
2、写者优先
除了上述四个规则外,还增加写者优先的规定,即当有读者和写者同时等待时,首先满足写者。当一个写者声明想写文件时,不允许新的读者再访问文件。
3、无优先
除了上述四个规则外,不再规定读写的优先权,谁先等待谁就先使用文件。
【实验原理】
【程序代码】
#include <windows.h>
#include <conio.h>
#include <stdlib.h>
#include <stdio.h>
#include <io.h>
#include <string.h>
#define MAX_THREAD 10 //待测试的线程数
typedef struct{ //表示测试数据格式char thread_name[3]; //线程名unsigned int require_moment; //请求操作时刻unsigned int persist_time; //操作持续时间
}TEST_INFO;
TEST_INFO test_data[MAX_THREAD]={ //测试数据表{"r1",0,15}, // r表示读者线程{"r2",1, 15}, //w表示写者线程{"w1",3,3},{"r3",4, 2},{"w2",5,6},{"w3",6,10},{"r4",7,8},{"r5",9,2},{"w4",10,18},{"w5",12,2}
};int read_count=0; //记录正在读文件的读者数
int write_count=0; //在写者优先算法中记录声明要写文件的写者数
CRITICAL_SECTION CS_DATA; //用于保护文件的临界区变量
HANDLE h_mutex_read_count=CreateMutex(NULL,FALSE,"mutex_read_count");
//读者计数器互斥体
HANDLE h_mutex_write_count=CreateMutex(NULL,FALSE,"mutex_write_count");
//写者计数器互斥体
HANDLE h_mutex_reader_wait=CreateMutex(NULL,FALSE,"mutex_reader_wait");
//在写者优先算法中用于阻塞读者的互斥体
HANDLE h_mutex_first_reader_wait=
CreateMutex(NULL,FALSE,"mutex_first_reader_wait");
//在写者优先算法中用于阻塞第一个读者的互斥体
HANDLE h_mutex_wait=CreateMutex(NULL,FALSE,"mutex_wait");
//无优先时用于阻塞读者和写者的互斥体//读者优先时的读者线程
void RF_reader_thread(void *data){char thread_name[3]; //存放线程名称strcpy(thread_name,((TEST_INFO *)data)->thread_name);Sleep(((TEST_INFO *)data)->require_moment*1000);WaitForSingleObject(h_mutex_read_count,-1);
//申请进入关于读者计数器的临界区相当于P操作read_count++;if(read_count==1)EnterCriticalSection(&CS_DATA); //申请进入关于文件的临界区相当于P操作ReleaseMutex(h_mutex_read_count);
//离开关于读者计数器的临界区相当于V操作printf("%s ",thread_name);Sleep(((TEST_INFO *)data)->persist_time*1000); //用延迟相应秒来模拟读文件操作WaitForSingleObject(h_mutex_read_count,-1);read_count--;if(read_count==0)LeaveCriticalSection(&CS_DATA);//离开关于文件的临界区相当于V操作ReleaseMutex(h_mutex_read_count);
}//读者优先时的写者线程
void RF_writer_thread(void *data){Sleep(((TEST_INFO *)data)->require_moment*1000);//用延迟相应秒来模拟写文件操作EnterCriticalSection(&CS_DATA);//申请进入,相当于P操作 printf("%s ",((TEST_INFO *)data)->thread_name);Sleep(((TEST_INFO *)data)->persist_time*1000); //用延迟相应秒来模拟写文件操作LeaveCriticalSection(&CS_DATA);//离开,相当于V操作
}//读者优先时的初始化程序
void reader_first(){int i=0;HANDLE h_thread[MAX_THREAD];printf("读优先申请次序:");for(i=0;i<MAX_THREAD;i++){printf("%s ",test_data[i].thread_name);};printf("\n");printf("读优先操作次序:");InitializeCriticalSection(&CS_DATA); //初始化临界区变量for(i=0;i<MAX_THREAD;i++){ //根据线程名称创建不同的线程if(test_data[i].thread_name[0]=='r') //名称的首字母是'r'则创建读者线程h_thread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(RF_reader_thread),&test_data[i],0,NULL);else //名称的首字母是'w'则创建写者线程 h_thread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(RF_writer_thread),&test_data[i],0,NULL);}WaitForMultipleObjects(MAX_THREAD,h_thread,TRUE,-1); //等待所有线程结束printf("\n");
}
//无优先时的读者线程
void FIFO_reader_thread(void *data){char thread_name[3];strcpy(thread_name,((TEST_INFO *)data)->thread_name);Sleep(((TEST_INFO *)data)->require_moment*1000);//用延迟相应秒来模拟写文件操作WaitForSingleObject(h_mutex_wait,-1);//申请进入 WaitForSingleObject(h_mutex_read_count,-1);//申请进入关于读者计数器的临界区相当于P操作read_count++;if(read_count==1)EnterCriticalSection(&CS_DATA);ReleaseMutex(h_mutex_read_count);//离开关于读者计数器的临界区相当于V操作ReleaseMutex(h_mutex_wait);//离开关于等待计数器的临界区相当于V操作printf("%s ",thread_name);Sleep(((TEST_INFO *)data)->persist_time*1000);//用延迟相应秒来模拟写文件操作WaitForSingleObject(h_mutex_read_count,-1);read_count--;if(read_count==0)LeaveCriticalSection(&CS_DATA);ReleaseMutex(h_mutex_read_count);//离开关于读者计数器的临界区相当于V操作
}//无优先时的写者线程
void FIFO_writer_thread(void *data){Sleep(((TEST_INFO *)data)->require_moment*1000);//用延迟相应秒来模拟写文件操作WaitForSingleObject(h_mutex_wait,-1);EnterCriticalSection(&CS_DATA);printf("%s ",((TEST_INFO *)data)->thread_name);Sleep(((TEST_INFO *)data)->persist_time*1000);//用延迟相应秒来模拟写文件操作LeaveCriticalSection(&CS_DATA);ReleaseMutex(h_mutex_wait);
}//无优先时的初始化程序
void first_come_first_served(){int i=0;HANDLE h_thread[MAX_THREAD];printf("无优先申请次序:");for(i=0;i<MAX_THREAD;i++){printf("%s ",test_data[i].thread_name);};printf("\n");printf("无优先操作次序:");InitializeCriticalSection(&CS_DATA);//初始化临界区变量for(i=0;i<MAX_THREAD;i++){//根据线程名称创建不同的线程if(test_data[i].thread_name[0]=='r')//名称的首字母是'r'则创建读者线程h_thread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(FIFO_reader_thread),&test_data[i],0,NULL);else//名称的首字母是'w'则创建写者线程h_thread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(FIFO_writer_thread),&test_data[i],0,NULL);}WaitForMultipleObjects(MAX_THREAD,h_thread,TRUE,-1); //等待所有线程结束printf("\n");
}//写者优先时的读者线程
void WF_reader_thread(void *data){char thread_name[3];strcpy(thread_name,((TEST_INFO *)data)->thread_name);Sleep(((TEST_INFO *)data)->require_moment*1000);//用延迟相应秒来模拟写文件操作WaitForSingleObject(h_mutex_reader_wait,-1);//申请进入,相当于P操作WaitForSingleObject(h_mutex_first_reader_wait,-1);//申请进入,相当于P操作WaitForSingleObject(h_mutex_read_count,-1);//申请进入,相当于P操作read_count++;if(read_count==1)EnterCriticalSection(&CS_DATA);ReleaseMutex(h_mutex_read_count);//离开,相当于V操作 ReleaseMutex(h_mutex_first_reader_wait);//离开,相当于V操作 ReleaseMutex(h_mutex_reader_wait);//离开,相当于V操作 printf("%s ",thread_name);Sleep(((TEST_INFO *)data)->persist_time*1000);//用延迟相应秒来模拟写文件操作WaitForSingleObject(h_mutex_read_count,-1);//申请进入,相当于P操作read_count--;if(read_count==0)LeaveCriticalSection(&CS_DATA);ReleaseMutex(h_mutex_read_count);//离开,相当于V操作
}//写者优先时的写者线程
void WF_writer_thread(void *data){Sleep(((TEST_INFO *)data)->require_moment*1000);//用延迟相应秒来模拟写文件操作WaitForSingleObject(h_mutex_write_count,-1);if(write_count==0)WaitForSingleObject(h_mutex_first_reader_wait,-1);write_count++;ReleaseMutex(h_mutex_write_count);//离开关于写者计数器的临界区相当于V操作EnterCriticalSection(&CS_DATA);printf("%s ",((TEST_INFO *)data)->thread_name);Sleep(((TEST_INFO *)data)->persist_time*1000);//用延迟相应秒来模拟写文件操作LeaveCriticalSection(&CS_DATA);WaitForSingleObject(h_mutex_write_count,-1); //申请进入,相当于P操作write_count--;if(write_count==0)ReleaseMutex(h_mutex_first_reader_wait);ReleaseMutex(h_mutex_write_count);//离开关于写者计数器的临界区相当于V操作
}//写者优先时的初始化程序
void writer_first(){int i=0;HANDLE h_thread[MAX_THREAD];printf("写优先申请次序:");for(i=0;i<MAX_THREAD;i++){printf("%s ",test_data[i].thread_name);}printf("\n");printf("写优先操作次序:");InitializeCriticalSection(&CS_DATA);for(i=0;i<MAX_THREAD;i++){if(test_data[i].thread_name[0]=='r')h_thread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(WF_reader_thread),&test_data[i],0,NULL);elseh_thread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(WF_writer_thread),&test_data[i],0,NULL);}WaitForMultipleObjects(MAX_THREAD,h_thread,TRUE,-1);printf("\n");
}//主程序
int main(int argc,char *argv[]){char select;while(1){printf("|-----------------------------------|\n");printf("| 1:reader first |\n");printf("| 2:first come first served |\n");printf("| 3:writer first |\n");printf("| 4:exit |\n");printf("|-----------------------------------|\n");printf("select a function(1~4):");do{select=(char)getch();}while(select!='1'&&select!='2'&&select!='3'&&select!='4');system("cls");switch(select){case '1':reader_first();break;case '2':first_come_first_served();break;case '3':writer_first();break;case '4':return 0;}printf("\nPress any key to continue.");getch();system("cls");}return 0;
}
【实验结果】
图1 初始界面
图2 读优先
图3 无优先
图4 写优先
【实验心得】
通过本次实验,我对操作系统的同步机制---读者写者问题有了更加深刻的认识,实验中也学到了很多知识,受益匪浅。读者写者问题在计算机领域非常普遍,数据库的读写分离就是为了减少因为读者写者问题加锁带来的对并发的影响。
读者写者问题的解决方案一般都有两种不同的侧重:读者优先或者写者优先。简单来说,读者优化就是尽量满足并发的读操作,当已经有线程在读数据的时候,其他读线程无需等待,而写线程需要等待所有正在进行的读操作之后才能执行。写者优先就是尽量满足写操作,尽管写操作不能并发,但是可以排队,优先于等待的读线程获得执行权。