操作系统概念 实验三:进程同步控制

年轻人要讲抄德,抄袭作业又蠢又坏,大家耗子尾汁
之前一直没公开来着

实验要求

实验要求及内容
利用信号量机制,实现读者写者问题的解决方案。 在 OpenEuler/Linux 环境下,创建一个控制台进程,此进程包含 n 个线程。
用这 n 个线程来表示 n 个读者或写者。每个线程按相应测试数据文件的要求进行读写操作。用信号量机制分别实现读者优先和写者优先的读者-写者问题。
读者-写者问题的读写操作限制(包括读者优先和写者优先):
写-写互斥,即不能有两个写者同时进行写操作。
读-写互斥,即不能同时有一个线程在读,而另一个线程在写。
读-读允许,即可以有一个或多个读者在读。
读者优先的附加限制:如果一个读者申请进行读操作时已有另一个读者正在
进行读操作,则该读者可直接开始读操作。
写者优先的附加限制:如果一个读者申请进行读操作时已有另一写者在等待访问共享资源,则该读者必须等到没有写者处于等待状态才能开始读操作。
运行结果显示要求:要求在每个线程创建、发出读写操作申请、开始读写操作和结果读写操作时分别显示一行提示信息,以确定所有处理都遵守相应的读写操作限制。

实验步骤
读者-写者 实验原理及步骤:

数据文件:
文件 Thread.txt 含有 n 行数据,每一行数据代表一个读写请求。比如:
R,100 //读请求,读操作的持续时间为 100ms
W,200 //写请求,写操作的持续时间为 200ms

持续时间可以自己设定。

临界区
可以定义一个共享环形队列,初始化队列长度为 100,初始化队列元素为 0。
写者进入临界区,向队尾添加一个随机数;读者进入临界区,从队首取走一个数据。

读者-写者模型

在学习操作系统的过程中,我们一共学习过三大主要模型:生产者-消费者模型、读者-写者模型、哲学家进餐模型。
本实验就是要代码实现读者-写者模型。如果不了解信号量是什么,可以找找这个博主
模型分为两类,实验要求也已经描述清楚,无论哪种都至少遵循写-写互斥,读-写互斥,读-读允许。那么两个问题的伪代码下:

读者优先
如果有新写者产生,那么它必须等待所有读者完成读操作后才能访问队列。

读者写者

写者优先
如果有新读者产生,那么它必须等待所有写者完成读操作后才能访问队列。

读者写者

推荐的环境

Linux上实在不容易下载安装编译器(比如装Clion还要先配gdk,博主懒,不想搞了)。在Win10下先用着Clion,调好了复制进去也不失为好选择。
pthread.h、unistd.h库一般情况下Clion的内核是可以直接调的,这也是我选择Clion的原因,除了usleep()函数在windows上运行会出现奇怪的睡眠时间错乱的问题(不太好用)以外其他的编译都没有任何问题,很离谱。
我不放P了,完成实验要用到的必要库:
线程操作<pthread.h>、时间把控<time.h>、信号量使用<semaphore.h>
对于整个环境的初始化,我采用了以链表实现环形队列的方案。(本来就是环形链表)

实现思路

在要求中,我们可以看到附加条件的描述。其实如果能够实现读者优先或者写者优先的话,附加条件是自然而然就可以满足了的。
思路有两种:

  1. 读一行,休眠一行
  2. 全读完,线程自行休眠

神魔意思呢?读一行休眠一行,就是说为了模拟线程在特定时刻才会出现(arrive),每读一行就在进程里休眠一会儿,直到那个线程的出现时刻到了才创建这个线程。全读完的方式则是直接一次性读完整个.txt并创建好所有线程,把到达时间、运行时间传给每个线程函数。
因为自打线程被创建伊始就会开始运行,而我们编写的程序仅仅是为了模拟,所以时间精度将会非常低。什么意思?意思就是采用方式1会不太可行。创建线程是耗时的,比如写者1写完了,文件中恰好同一时刻出现了写者、读者请求,但是.txt中恰好读者是写者上一行……然后的故事,你可以试试,这个写者真的很难抢过它。main函数自己一个一个辛辛苦苦创建线程远不如直接让各个线程自己睡一觉再醒来的同步程度高。
为什么同样是考虑创建线程的耗时,第二种方法会更好一些呢?博主觉得可能和进程、线程的关系有关。线程自己休眠不会干扰其他线程,但是在main函数里休眠,怎么说呢,就真的感觉是在阻塞整个进程的线程似的,闹到最后就是出现上述情况,Sleep(0)也不管事,因为调用Sleep(0)的过程中原本写者的活儿就真被抢了,导致各个线程只能一个一个按txt的顺序创建并执行。

如何把多个参数传给线程函数

其实最开始我并不是很懂pthread_create()函数最后那个参数到底要来做什么,但是这时候好的编译器的辅助学习作用就体现出来了:Clion自动为我提示那个参数是“arg”,显然之前写的博客里,线程函数的参数param就是传递的参数。
如上文所述,为了更好地模拟线程们的arrival time这一项,在创建线程是就要为每个线程加入这个属性。也就是说为了让一个线程强制在某一时刻醒来并强制运行指定时间,就需要给线程函数传两个参数。传一个参数还好说,强转为void指针就行,在函数里强转回来就可以得到该值。两个甚至三个参数,已经违背了pthread_create()只允许传一个参数的默认设置了,所以,可以采用结构体!
只需要定义一个结构体,把两个甚至更多参数放进去,再把结构体地址以void指针形式传给线程函数,在函数内强转回结构类型,就可以取出这些数据了。

//定义参数结构体
typedef struct Params{
    int time_start;
    int time_pro;
}Params;
/*
在某处设置参数,如下:
Params *params = NULL;
params = (Params*)malloc(sizeof(Params));
params->time_start = time_wait;
params->time_pro = time_process;
pthread_create(&thread, &attr, (void *(*)(void *)) RP_WriterThread, (void *)params);
*/
void *RP_WriterThread(void *param){
    Params * time_set = NULL;
    time_set = (struct Params *)param;
	//之后就可以直接用time_set->timestart和time_pro取出自己设的参数值了
}

理论分析:创建我的线程顺序

类型安排:arrive, process, next
W 00 500 P
R 1100 100 P
R 2150 500 P
W 3400 400 P
R 4500 1400 P
R 5600 200 P
W 65600 2000 P
R 77000 1500 P
R 87500 300 $

Gantt图

声明一下,这俩Gantt图放错了,没有W6奥,这俩图对应的其实是文末的那俩运行结果

读者优先
在这里插入图片描述

写者优先
在这里插入图片描述

实验代码以及结果验证

读者优先

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <semaphore.h>
#include <unistd.h>
//传递参数的结构体格式
typedef struct Params{
    int time_start;		//指示线程先行休眠再唤醒的时间
    int time_pro;		//指示线程强行阻塞自己的时间
}Params;
//缓冲区的结构体格式
typedef struct buffer {
    int id;				//标识一下队列某一个链节的序号
    int num;			//队列被写的内容
    struct buffer *next;
}buffer;
//线程队列,每个链节内就存放着运行的线程
typedef struct Queue{
    pthread_t who;		//线程者谁
    pthread_attr_t the_attr;	//线程资源者谁
    struct Queue *next;
}Queue;

buffer *headBuffer = NULL;      //缓冲区队列
Queue  *headQueue = NULL;       //线程队列
sem_t RP_Write, r_mutex;        //信号量
int read_count=0;               //读者数量

//用于写者读写缓冲区,插入新数据
buffer* insert_item(int number){
    buffer *headPtr = headBuffer, *currentPtr = NULL;
    int i = 0;
    currentPtr = headPtr;
    while(i<100 && currentPtr->num != 0){
        currentPtr = currentPtr->next;
        i++;                                //由于是环形100队列,所以要防止无限循环
    }
    if(currentPtr->next==headPtr)			//队列满
        printf("insert failed\n");
    else{
        printf("insert %d to id:%d success!\n",number, currentPtr->id);
        currentPtr->num = number;
    }
    return headPtr;
}
//读者读取缓冲区
void fetch_item(int *id, int *fetcher){
    *id = headBuffer->id;
    *fetcher = headBuffer->num;
    if(headBuffer->num!=0){
        headBuffer->num = 0;
        headBuffer = headBuffer->next;
    }
}
//写者线程,参数是txt第二列的值,指示ms值
void *RP_WriterThread(void *param){
    Params * time_set = NULL;
    time_set = (struct Params *)param;

    usleep(time_set->time_start * 1e3);
    printf("Writer wants to write\n"); 

    sem_wait(&RP_Write);                        //等待允许操作缓冲区

    srand(time(0));                				//根据时间产生随机种子
    int random = rand()%1000;                   //生成随机的数据
    usleep(time_set->time_pro * 1e3);
    headBuffer = insert_item(random);

    sem_post(&RP_Write);                        //释放对缓冲区控制权
    pthread_exit(NULL);                     	//由于是一次性线程执行,不用返回值给pthread_join
}
//读者线程,参数是txt第二列的值,指示ms值
void *RP_ReaderThread(void *param){
    Params * time_set = NULL;
    time_set = (struct Params *)param;
    int id = 0, number = 0;                     //记录缓冲区取出数据的信息

    usleep(time_set->time_start * 1e3);
    printf("Reader wants to read\n"); 

    sem_wait(&r_mutex);
    read_count++;
    if(read_count==1)
        sem_wait(&RP_Write);
    sem_post(&r_mutex);
    fetch_item(&id, &number);

    usleep(time_set->time_pro * 1e3);
    printf("Reader%d has read---->id:%d,number:%d\n\n", read_count,id, number);

    sem_wait(&r_mutex);
    read_count--;
    if(read_count==0)
        sem_post(&RP_Write);
    sem_post(&r_mutex);
    pthread_exit(NULL);                     	//由于是一次性线程执行,不用返回值给pthread_join
}
//初始化一个100大小的环形队列
buffer* init_queue(buffer *head){
    buffer *headPtr = NULL, *currentPtr = NULL, *lastPtr = NULL;
    headPtr = head;
    for(int i =0; i < 100; i++){
        currentPtr = (buffer *)malloc(sizeof(buffer));
        currentPtr->id = i;
        currentPtr->num = 0;
        if(headPtr == NULL){
            headPtr = currentPtr;
            lastPtr = headPtr;
        }
        else{
            lastPtr->next = currentPtr;
            lastPtr = lastPtr->next;
        }
        currentPtr->next = headPtr;
    }
    return headPtr;
}
//创建一个新的线程
Queue* create_Thread(const char *c, struct Params* params){
    Queue *headPtr = NULL, *currentPtr = NULL, *lastPtr = NULL;
    headPtr = headQueue;
    currentPtr = (Queue *)malloc(sizeof(Queue));
    pthread_attr_init(&currentPtr->the_attr);
    currentPtr->next = NULL;
    if(headPtr == NULL){
        headPtr = currentPtr;
    }
    else{
        lastPtr = headPtr;
        while(lastPtr->next!=NULL)
            lastPtr = lastPtr->next;
        lastPtr->next = currentPtr;
    }
//根据读写者的不同创建线程
    if(*c=='R')
        pthread_create(&currentPtr->who, &currentPtr->the_attr, (void *(*)(void *)) RP_ReaderThread, (void *)params);
    else
        pthread_create(&currentPtr->who, &currentPtr->the_attr, (void *(*)(void *)) RP_WriterThread, (void *)params);
    return headPtr;
}
int main(int argc, char *argv[]) {
    sem_init(&RP_Write, 0, 1);
    sem_init(&r_mutex, 0, 1);
    headBuffer = init_queue(headBuffer);
    FILE *fp;
    char c,go;
    int time_wait = 0, time_process = 0;
    //读文件,创建好整个请求队列
    if((fp = fopen("Thread.txt","r"))==NULL){
        printf("Can't open the file\n");
    }
    else{
        while(!feof(fp)){
            fscanf(fp,"%c %d %d %c",&c, &time_wait, &time_process, &go);
            fgetc(fp);
            Params *params = NULL;
	    params = (Params*)malloc(sizeof(Params));
	    params->time_start = time_wait;
	    params->time_pro = time_process;
	    headQueue = create_Thread(&c, params);
            if(go=='$'){
			  break;  
		    }
        }
    }
	fclose(fp); 
	//自行等待线程们运行
    printf("All Instruction Read, input one symbol to exit\n");
    getchar();
}

在这里插入图片描述
在所有线程创建好后,最先出现的是一个写者线程,因此显示出”insert 835 to id:0 success”这句话,说明这是首位写者成功写入了数据。在写者工作的这500ms内,R1、R2、W3、R4依次出现,发出请求。
随后连续四个读者,对应Gantt图中的R1、R2、R4、R5,由于我设置的是强行阻塞线程一段时间后再输出读取出的缓冲区单元结果,所以这四个读者的线程都是正确执行了的。由于此时队列里只有一个元素,所以只有四个读者之首R1读出了非0数字。
由于是读者优先,W3线程虽然比R4、R5到达的早,但是W1写者写完后恰好是第500ms,因此W3未优先于R4,也因此一直等到四个读者读完才得以执行自己的操作。
W6执行完后,随之而来的依次是R7、R8,R7先进入的缓冲区,所以R8是要取走当前队首的的,而R8则取走,因为R8读操作更快,所以先于R7输出自己的结果,这也直接验证了读者可以同时访问缓冲区的附加条件。

写者优先

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <semaphore.h>
#include <unistd.h>
typedef struct Params{
    int time_start;
    int time_pro;
}Params;
//缓冲区的结构体格式
typedef struct buffer {
    int id;
    int num;
    struct buffer *next;
}buffer;
typedef struct Queue{
    pthread_t who;
    pthread_attr_t the_attr;
    struct Queue *next;
}Queue;

buffer *headBuffer = NULL;      //缓冲区队列
Queue  *headQueue = NULL;       //线程队列
//信号量:mutex
sem_t r_mutex,w_mutex,queue_mutex,RP_Write,can_mutex;
//读者写者数量
int read_count=0,write_count=0;
buffer* insert_item(int number){
    int i = 0;
    buffer *headPtr = headBuffer, *currentPtr = NULL;
    currentPtr = headPtr;
    while(i < 100 && currentPtr->num != 0){
        currentPtr = currentPtr->next;
        i++;
    }
    if(currentPtr->next==headPtr)
        printf("insert failed\n\n");
    else{
        printf("insert %d to id:%d success!\n\n",number, currentPtr->id);
        currentPtr->num = number;
    }
    return headPtr;
}
//用于读者读取缓冲区
void fetch_item(int *id, int *fetcher){
    *id = headBuffer->id;
    *fetcher = headBuffer->num;
    if(headBuffer->num!=0){
        headBuffer->num = 0;
        headBuffer = headBuffer->next;
    }
}
//写线程
void *RP_WriterThread(const int *param){
    Params * time_set = NULL;
    time_set = (struct Params *)param;

    usleep(time_set->time_start * 1e3);
    printf("Writer%d wants to write\n",write_count); 

    sem_wait(&w_mutex);
    write_count++;
    if(write_count==1)
        sem_wait(&queue_mutex);                 //只要有写者,就等待最后一个读者读完,然后禁掉其后所有的读者请求
    sem_post(&w_mutex);
    sem_wait(&RP_Write);                        //等待允许操作缓冲区

    srand(time(0));                				//根据时间产生随机种子
    int random = rand()%1000;                   //生成随机的数据
    headBuffer = insert_item(random);

    usleep(time_set->time_pro * 1e3);

    sem_post(&RP_Write);                        //释放对缓冲区控制权
    write_count--;
    if(write_count==0)
        sem_post(&queue_mutex);
    sem_post(&w_mutex);
    pthread_exit(NULL);
}
//读者线程
void *RP_ReaderThread(const int *param){
    Params * time_set = NULL;
    time_set = (struct Params *)param;
    int id = 0, number = 0;

    usleep(time_set->time_start * 1e3);
	printf("Reader wants to read\n");
	
    sem_wait(&queue_mutex);
    sem_wait(&r_mutex);
    read_count++;
    if(read_count==1)
        sem_wait(&RP_Write);
    sem_post(&r_mutex);
    sem_post(&queue_mutex);
    fetch_item(&id, &number);

    usleep(time_set->time_pro * 1e3);
    printf("Reader%d has read---->id:%d,number:%d\n\n", read_count,id, number);

    sem_wait(&r_mutex);
    read_count--;
    if(read_count==0)
        sem_post(&RP_Write);
    sem_post(&r_mutex);
    pthread_exit(NULL);
}
//初始化一个100大小的环形队列
buffer* init_queue(buffer *head){
    buffer *headPtr = NULL, *currentPtr = NULL, *lastPtr = NULL;
    headPtr = head;
    for(int i =0; i < 100; i++){
        currentPtr = (buffer *)malloc(sizeof(buffer));
        currentPtr->id = i;
        currentPtr->num = 0;
        if(headPtr == NULL){
            headPtr = currentPtr;
            lastPtr = headPtr;
        }
        else{
            lastPtr->next = currentPtr;
            lastPtr = lastPtr->next;
        }
        currentPtr->next = headPtr;
    }
    return headPtr;
}
//创建一个新的线程
Queue* create_Thread(const char *c, struct Params* params){
    Queue *headPtr = NULL, *currentPtr = NULL, *lastPtr = NULL;
    headPtr = headQueue;
    currentPtr = (Queue *)malloc(sizeof(Queue));
    pthread_attr_init(&currentPtr->the_attr);
    currentPtr->next = NULL;
    if(headPtr == NULL){
        headPtr = currentPtr;
    }
    else{
        lastPtr = headPtr;
        while(lastPtr->next!=NULL)
            lastPtr = lastPtr->next;
        lastPtr->next = currentPtr;
    }

    if(*c=='R')
        pthread_create(&currentPtr->who, &currentPtr->the_attr, (void *(*)(void *)) RP_ReaderThread, (void *)params);
    else
        pthread_create(&currentPtr->who, &currentPtr->the_attr, (void *(*)(void *)) RP_WriterThread, (void *)params);
    return headPtr;
}

int main(int argc, char *argv[]) {
    sem_init(&r_mutex, 0, 1);
    sem_init(&w_mutex, 0, 1);
    sem_init(&queue_mutex,0,1);
    sem_init(&RP_Write,0,1);
    sem_init(&can_mutex,0,1);
    headBuffer = init_queue(headBuffer);
    FILE *fp;
    char c, go;
    int time_wait = 0, time_process = 0;
    //读文件,创建好整个请求队列
    if((fp = fopen("Thread.txt","r"))==NULL){
        printf("Can't open the file\n");
    }
    else{
        while(!feof(fp)){
            fscanf(fp,"%c %d %d %c",&c, &time_wait, &time_process, &go);
            fgetc(fp);
            Params *params = NULL;
	    params = (Params*)malloc(sizeof(Params));
	    params->time_start = time_wait;
	    params->time_pro = time_process;
	    headQueue = create_Thread(&c, params);
            if(go=='$'){
			  break;  
		    }
        }
    }
    fclose(fp); 
    printf("All Instruction Read, input one symbol to exit\n");
    getchar();
}

在这里插入图片描述
在所有线程创建好后,最先出现的是一个写者线程,因此显示出”insert 249 to id:0 success”这句话,说明这是首位写者成功写入了数据。
随后又执行了一个写者,这就是W3,虽然W3出现的晚于R1、R2,但是由于W1直到500ms才执行完,所以W3在这之前就已经改写了write_count,导致W1结束后不会signal(queue_mutex),因此所有读者们还是需要继续排队,直到所有写者完成自己的操作。这直接证明了写者优先的附加条件。
随后连续四个读者,对应Gantt图中的R1、R2、R4、R5,同样由于强行阻塞,所以这四个读者中按照R1、R2、R4、R5依次访问缓冲区,成功读取的R1、R2线程是可以取出非0数字的,从Gantt图中,我们可以看见输出结果的顺序上是R1、R5、R2、R4,所以667、0、77、0的顺序是正确对应着的。
随后的W6、R7、R8和上面读者优先的情况一样。

以上就是本次实验大体过程,希望对大家有所帮助。

??? 居然还需要加关于时间的内容。。。可以用timeval辅助记录一段代码到底运行了多久。另外还需要标记一个线程具体的开始时间和结束时间,借此完成。代码略了。

最终利用的Thread.txt:

类型安排:arrive, process, next
W0 500 P
R100 100 P
R150 500 P
W400 400 P
R500 1400 P
R600 200 P
W800 1000 P
W5600 2000 P
R7000 1500 P
R7500 300 $

最终取得的效果

以上就是本次实验大体过程,又双叒叕希望对大家有所帮助!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值