操作系统笔记:读者写者问题,哲学家问题

除了生产者消费者问题外,读者写者问题和哲学家问题也是有关进程间互斥和同步的例子。

读者写者问题

  • 问题描述
    磁盘上有一个文件,规定此文件可同时被多个读者打开读取,而写者写入时只能供一个写者写,写者写时不可读,读者读时不可写,写一个程序来实现上述互斥与同步关系。

  • 问题分析与实现方法

文件对于读者和写者这两个整体之间,是互斥使用的;对于写者与写者之间,也是互斥使用的;但对于读者与读者之间,此文件可无需互斥使用。因而可以追加一个文件锁mutex并设置其初值为1,来实现其中的互斥关系

只要有一个或一个以上的读者在读文件,那么其他的读者随时都可以打开文件进行读操作,所以一个读者判断当前文件是否可以读的条件是:目前读此文件的读者数量是否大于或等于1? OR 文件锁mutex的值是否为1? 其中mutex为1表示此时文件既没有被读者读,也没有被写者写。所以此时就需要增加一个变量readerCount来表示当前读此文件的读者数量,若某读者申请到了读文件的机会,那么此时readerCount就必须加1;当某读者结束文件的读取时,readerCount的值相应减1;

其中一位读者结束文件读取后,readerCount减1变为0,则表示此文件已没有读者在读取,此时读者应该释放文件锁mutex,mutex值加1

  • 程序源码
#include 
#include 
#include  
#include  
#include 

#define 	RECORD_LEN 64
#define 	BUF_SIZE 4096
#define		TEST_FILE "rw.log"

void *reader(void *argu);
void *writer(void *argu);
unsigned int sleepTime();

int				readerCount = 0;
sem_t			sem_mutex; 

int main(int argc, char **argv)
{	
	int		reader_count, writer_count;
	int 		r = 0;
	int 		w = 0;
	FILE		*file = fopen(TEST_FILE, "a");

	if(!file)
	{
		printf("Fail to open the file %s\n", TEST_FILE);
		printf("Abort\n");
		return -1;
	}

	sem_init(&sem_mutex, 0, 1);

	while(1)
	{
		printf("Please input the quantity of reader:\n");
		scanf("%d", &reader_count);
		printf("Please input the quantity of writer_count:\n");
		scanf("%d", &writer_count);
		if(reader_count<=0 || writer_count<=0)
		{
			printf("\nInvalid arguments: Both of the quantity must be two positive integer!\n");
			printf("Please input again:\n");
		}
		else{
			break;
		}
	}	
	
	pthread_t			*thread_read = (pthread_t *)malloc(reader_count*sizeof(pthread_t));
	pthread_t			*thread_write = (pthread_t *)malloc(writer_count*sizeof(pthread_t));
	
	while(r!=reader_count && w!=writer_count)
	{
		pthread_create(&thread_read[r], NULL, reader, NULL);
		pthread_create(&thread_write[w], NULL, writer, NULL);
		++r;
		++w;
	}
	
	printf("\n*****All threads have been created successfully!*****\n\n");

	while(1);
	
	return 0;
}

void *reader(void *argu)
{
	FILE		*file;
	char 		buffer[BUF_SIZE];
	memset(buffer, 0, BUF_SIZE);
	int 		size = 0;

	while(1)
	{
		if(!readerCount)
			sem_wait(&sem_mutex);
		readerCount++;
		
		file = fopen(TEST_FILE, "r");
		fseek(file, 0, SEEK_SET);
		size = fread(buffer, sizeof(char), BUF_SIZE, file);
		printf("$$$Reader [%lu] read %d bytes from %s\n", pthread_self(), size, TEST_FILE);
		if(size >= BUF_SIZE){
			fopen(TEST_FILE, "w+");
			printf("$$$File %s has been truncated by reader [%lu] due to the oversize\n", TEST_FILE, pthread_self());
		}
		memset(buffer, 0, BUF_SIZE);
		fclose(file);

		readerCount--;
		if(!readerCount)
			sem_post(&sem_mutex);
		
		sleep(sleepTime());
	}
}

void *writer(void *argu)
{
	FILE		*file;
	char		record[RECORD_LEN];
	memset(record, 0, RECORD_LEN);
	int 		count = 0;

	while(1)
	{
		sem_wait(&sem_mutex);

		if( !(file=fopen(TEST_FILE, "a")) ){
			printf("###Writer [%lu] Fail to open the file %s\n", pthread_self(), TEST_FILE);
			continue;
		}
		snprintf(record, RECORD_LEN, "Writer [%lu] has written the file for %d times\n", pthread_self(), ++count);
		fputs(record, file);
		fclose(file);	
		printf("###%s", record);

		sem_post(&sem_mutex);

		sleep(sleepTime());
	}
}

unsigned int sleepTime(){
	unsigned int	sleep_time;
	while(1)
	{
		time_t	times = time(NULL);
		srand(times);
		sleep_time = rand()%10;
		if(sleep_time > 0 && sleep_time < 5)
			return sleep_time;
	}	
}	 
  • 源码逻辑
    此代码相对于生产者与消费者问题来说更易编写,而且基本格式同生产者与消费者问题类似。都是将题目中的读者与写者模拟为线程。创建一个文件供读者和写者读写,读者读完之后打印输出读到的文件的字节数,写者写完之后打印一下是第几次写此文件;读者与写者打印信息是还得要报告一下自己的线程号(调用pthread_self()获得);当文件写入的字节数超过4096bytes时,读者读取是就必须进行回滚,于是代码中就有了“fopen(TEST_FILE, "w+”)"这句话。

哲学家问题

  • 问题描述
    现有N个哲学家共用一张圆桌,分别坐在周围的N张椅子上,在圆桌上有N个碗和N只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。写一个程序来实现上述互斥与同步关系。

  • 问题分析与实现方法

问题中所说的筷子就是一种临界资源,在一段时间内只允许一个哲学家使用,由于一个哲学家只能拿与自己临近的筷子,所以每个哲学家吃饭时所拿的筷子是确定的,此时不妨用一个大小为N的信号量数组来表示筷子,并且把每个哲学家进行编号,其序号的范围从0到(N-1)

对每一个哲学家来说,自己吃饭所拿筷子的下标就应该是phi_id(自己的编号),和(pid_id+1)%N,这样也就确定了程序的大致写法。

这里需要考虑到一个可能的死锁问题,如果N个哲学家同时去拿自己左手边的筷子,那么最终就是每个哲学家都只能拿到一只筷子,另一只筷子也就无从获得了,从而死锁。针对这种情况,可以从拿筷子的动作、吃饭的顺序方面来解决。例如,可以使用AND型信号量机制,只有当一个哲学家同时拿到两只筷子才可以吃饭,倘若只拿到一只筷子,则立即放下这只筷子,在此去拿;还可以规定编号为偶数的哲学家先拿自己右手边的筷子,编号为奇数的哲学家先拿自己左手边的筷子,拿到第一只筷子的哲学家再去尝试拿第二只筷子;也可以设置一个信号量mutex,来表示拿筷子的机会,初值设为1,只有申请到拿筷子的机会之后,才可去拿筷子,拿到两只筷子之后,mutex必须释放,之后就可以吃饭了;

  • 程序源码
#include 
#include 
#include 
#include 

unsigned int sleepTime();
void *philosopher(void *argu);
void think();
void eat(); 

int 				philosopherCount;
sem_t				sem_mutex, *sem_chopstick;

int main(int argc, char **argv){
	int				phi;
	pthread_t		*thread_phi;
	
	while(1){
		printf("Please input the quantity of philosopher:\n");
		scanf("%d", &philosopherCount);
		if(philosopherCount > 1)
			break;
		else
			printf("Invalid argument: the quantity of philosopher must be a positive integer greater than 1!" 
                    "\nInput the data again\n");
	}
	
	sem_chopstick = (sem_t *)malloc(sizeof(sem_t)*philosopherCount);
	thread_phi = (pthread_t *)malloc(sizeof(pthread_t)*philosopherCount);
	
	sem_init(&sem_mutex, 0, 1);
	for(phi=0; phi 0 && sleep_time < 5)
			return sleep_time;
	}	
}	 
  • 源码逻辑

本人使用的是设置信号量mutex的方式。程序一开始先定义一个长度为philosopherCount的变长数组sem_chopstick[philosopherCount],之后再创建philosopherCount个线程,其中第phi个线程操作sem_chopstick[phi]和sem_chopstick[(phi+1)%philosopherCount] 。线程中传入的参数就是phi,此程序算是所有进程互斥与同步问题中比较难写的一个,除了要清楚算法以外,还要注意线程创建时传参数的方式,本人在给上述线程传参数的过程中犯过一个很难发现的错误,那就是创建完一个线程之后不能直接去创建下一个线程,否则之后的phi值会混乱,从而引起这个线程的崩溃,此时需要延时1秒,这也就出现了pthread_create()之后sleep(1)的语句;

除此之外,线程传入的参数必须以普通变量的方式使用,这也就是出现语句int id = *(int *)argu;的原因。使用普通变量的原因在于,指针所指的那片区域是在不断变化的,指针argu指向的区域是主线程main()中phi的存储单元,当主线程main()中phi变化时,*argu的值也就跟着变了,而子线程philosopher()中的id是要求不变的!

除了上面两处需要注意的地方外,其他地方的书写与前两个程序的思路大同小异。

后记

进程同步与互斥问题还有很多,汤姓老师编写的教科书里只举了三个例子。但生活中与此相关的问题比比皆是,例如:双向过独木桥问题、单跑道滑翔机降落起飞问题、交通信号灯问题等等…

相关的问题虽然很多,但把握其中的共性很重要,那就是仔细分析出问题中的临界资源和互斥同步关系,这是解决此类问题的关键所在。

今后碰到相关的问题时,本人就不再以博客的形式展示了,都以文件资源的方式分享。希望本人两篇博客中的思想方法和写程序的经验教训可以给读者们些许帮助!

你可能感兴趣的:(计算机基础理论)