前言
实验二相比实验一难度有所提升,首先得先掌握好相应的理论知识(读者-写者问题和消费者-生产者问题),才能在实验中得心应手。任务二的代码编写可以借鉴源码,所以我们要先读懂源码。
1.实验目的
掌握Linux环境下,进程(线程)同步以及临界资源的互斥访问方法。
2.实验要求
熟悉Linux操作系统线程创建、利用信号量机制进行同步和互斥访问临界资源。
3.实验内容
本实验包含生产者和消费者之间的同步以及读者和写者之间的同步两部分,请实现以下功能:
- 有多个生产者和多个消费者线程。我们使用含有n个元素的数组表示n个缓冲区。生产者线程每间隔一段时间例如一秒钟生产一个产品并放入缓冲区中。消费者线程等待生产者线程放入产品后在缓冲区中取出产品。请使用信号量机制实现生产者线程和消费者线程之间的同步。(生产者消费者)
- 有一个或多个写者线程和多个读者线程。读者和写者之间共享一缓冲区(缓冲区可用字符数组表示),写者获得缓冲区访问权限后往缓冲区写入内容。读者获得缓冲区访问权限后从缓冲区中读出内容。当有一个读者获得缓冲区访问权限后,其它读者线程可以访问该缓冲区。而读者与写者之间需要互斥的访问缓冲区。
4.实验的内容与过程
阅读代码
在实验前,我们先把代码理清楚。
mainProg.c
解析:在mainProg.c中,我们根据定义的生产者和消费者数量生成了相应的空间大小。紧接着,我们又定义了一个生产者消费者管理者,这个的主要作用是管理临界资源。(后面我会分析)接着就是两个for循环,一个for循环是调用两个生产者执行生产任务,一个for循环是调用消费者执行消费任务。在这两个循环中,我们都用到了pthread_create函数,这个函数主要作用是创建以第三个参数start_routine为入口函数的线程。传入的四个参数分别为:线程的指针、线程属性、线程运行函数起始地址、传入到运行函数的参数。也就是生产者的for循环中产生的子线程完成producerThread函数中的任务,消费者的for循环中产生的子线程完成consumerThread函数中的任务。
producerThread在Producer.c文件中
解析:ProducerThread函数中,先获取了我们的临界资源,然后随机生成种子数,为我们后面生成随机数做准备。接着在while循环中生产产品,直到exit_flag变成1,我们才不继续生产产品。生产产品我们主要调用了自定义的generateProduct函数。在generateProduct函数中,我们先将三个信号赋值上相应的数值,然后随机生成一个随机数,也就是我们的产品。接着调用semwait (empty)和semWait(mutex)。我们应该注意把semwait (empty)放在 semWait(mutex)前面,否则会产生死等现象。接着现有产品数量+1,再把产品放到缓存区中,并输出相应的信息。最后我们在线程结束后,调用semsignal释放信号。
consumerThread在Consumer.c文件中
解析:consumerThread函数跟上面的ProducerThread函数类似,并且图中也有注释,我就不详细介绍了。接着就是getProduct函数。在函数中,先将三个信号赋值上相应的数值,然后调用semwait (full)和semWait(mutex)并取出一个商品,然后产品数量减1并打印出相应的信息。最后我们在线程结束后,调用semsignal释放信号。
ProducerConsumerUtilities.c文件:
解析:在这个文件中,通过截屏中的注释,我们发现这个文件中无非是定义了一堆初始化和自定义的创建线程的方法,接着就是wait、signal、摧毁操作,方便我们在其他地方调用这些方法。
ProducerConsumerUtilities.h文件:
解析:在这个文件中定义了生产者消费者管理者这个结构体,其内容主要有记录mutex、full、empty这三个信号,生产者的偏移量,消费者的偏移量,现有产品的数量,缓存区,缓存区大小以及判断是否结束的变量。之所以定义这个结构体,是因为这些资源都是公共资源,即临界资源,是各个线程一起使用的。然后就是线程结构体和一些方法的声明。
至此,我们的代码关键部分基本已经分析完毕了,对实验的原理已经有了一定的了解,接下来就可以开始实验了。
任务1.写出生产者消费者程序对应Makefile
Makefile文件
解析:根据我们的现有文件生成对应的.o文件,即第三行的文件名。(注意第二行的CFLAGS变量中加入-lpthread)
运行结果:
可以看到,我们的生产者消费者程序已经可以运行了,输入q即可停止。(因为mainProg中定义了我们输入q的话exit_flag会变成1)
至此,我们的任务一就完成了,接下来就是任务二。
任务2:读者写者程序
mainProg.c
解析:定义了2个写者和6个读者后(个数自行定义),给相应的线程分配足够的空间,并生成一个读者写者的管理者,用来管理临界资源。接着就是写者线程和读者线程的初始化,和生产者消费者的代码大同小异,主要还得看下面部分的代码。
ReaderWriterUtilities.h文件:
解析:这个头文件主要定义了读者写者程序的管理者的一些方法和结构。首先是读者写者管理者的结构体。这个结构体里面有信号量mutex和rmutex(mutex信号用来实现互斥访问缓冲区,rmutex信号主要用来实现互斥访问readcount变量),还有readcount变量用来存放读者的数量,buffer数组用来存放字符,类似于写者的作品。还有buffersize用来记录缓存区大小,exit_flag来确定线程的状态。接着是线程结构体还有一些方法的定义。由于这些内容跟生产者消费者的代码部分大同小异,就不再赘述。
ReaderWriterUtilities.c文件:
解析:可以看到,在这个文件中,主要是定义了一堆初始化和自定义的创建线程的方法,接着就是wait、signal、摧毁操作,跟任务一代码类似并且截屏中已有注释,不赘述。接下来就是关键的读者部分和写者部分代码了。
Reader.c文件:
分析:先看看在这个文件中定义的read_data方法。这个方法主要先获取mutex信号、rmutex信号和readcount的值。然后定义了buf数组,用来存放作品的内容。接着,先使用wait语句来保证线程可以互斥访问readcount变量。如果readcount的值为0则说明当前没有读者在阅读作品,就先互斥访问临界资源来保证读者写者不会同时对临界资源进行操作(类似于第一个读的人要先去把作品拿过来),接着就是将readcount数量加1并且释放掉rmutex信号。当读者阅读完后,我们先互斥访问readcount,将readcount先减1,如果readcount的值为0,说明现在是最后一个读者,要释放掉mutex信号(类似于最后一个读完的人要归还作品),接着再释放掉rmutex信号。文件中定义的另一个方法readerThread则是实现读者线程的运行,如果exit_flag为0,则一直进行读操作。
Writer.c文件:
分析:先看这个文件中定义的write_data方法。在方法中,先获取mutex信号的值,然后调用wait方法,保证临界资源的互斥访问。接着,就在循环中往缓冲区写入字符,先随机获得一个偏移量(0到26),然后这个偏移量加上字符‘a’的ASCII码值就能得到‘a’到‘z’的一个字符。当我们把缓冲区填完了,就实现了一个写者的书写任务,接着就输出相应的信息并释放掉mutex信号。另一个方法writerThread方法则是实现写者线程的运行,当exit_flag的值为0时就一直执行写操作。
makefile文件:
解析:生成文件为ReaderWriterUtilities.o、Writer.o、Reader.o和mainProg.o,在第三行填入即可,跟任务一操作类似。
运行结果:
可以看到,我们的读者写者程序已经可以运行了,输入q即可停止。(因为mainProg中定义了我们输入q的话exit_flag会变成1)。通过观察,我们发现读者阅读的内容总跟最近一个写者书写的内容一样,说明代码是没问题的。
至此,我们的任务二也完成了。
要注意的是,writer.h和reader,h这两个头文件我没截屏出来,自己编写就行了,很简单的,仿照任务一的代码即可。
至此,我们的实验大功告成!如果大家有什么想法,可以在评论区提出,一起交流。