一、实验目的
1、掌握基本的多线程编程技术
2、掌握基本的线程间同步技术
3、熟练使用pthread线程库调用接口
二、实验内容
编写多线程程序解决读者-写者问题:
1. 至少有3个读者和2个写者
2. 每个读者写者进出数据集都要求打印输出信息
3. 实现读者优先策略
4. 实现写者优先策略
三、实验过程、结果与分析
首先根据要求在进入程序的初始线程中创建2个写者3个读者,读者线程的入口函数定义为void reader(),写者线程的入口函数定义为void writer(),在读者线程中的代码包含真正的读操作函数定义为read()和用于线程同步的代码,以及为了观察读写顺序的打印输出,写者线程中的代码包含真正的写操作函数定义为write()和用于线程同步的代码,以及为了观察读写顺序的打印输出。
因此整个程序基本结构如下:
主要问题在于如何通过在读者和写者线程reader()与writer()中通过线程同步实现两种不同的读写策略。
(一).读者优先的实现
1. 读者优先情况的要求分析:
(1)一个写者不能和其他写者或读者同时访问数据集;
(2)多个读者可以同时读数据集;
(3)如果有写者等待写,读者不需要等待。
对于要求(1),要实现写者和其他读者写者均互斥,在写者线程中可以采用互斥锁在写操作区上锁,如下:
同时在读者线程中也要获得写者互斥锁&writeLock后才能进行读操作,但若采取如下的结构,将违背第二条要求使得多个读者不能同时读数据集;
因此需要调整将解锁操作放在read()之前,或者让第二个及后续所有读者读时不需要获得该互斥锁,如果采用第一种方法将导致读者读操作进行时可能被写者打断,与要求(1)违背,因此考虑采用第二种方式。
这就需要一个计数量来判断此时到来的读者为第几个(这里所说的第几个为未退出读者线程的读者),定义为readerCnt;在每个读者到来时给该变量+1,在一个读者线程的读操作结束后即将退出该线程时,再将该变量-1,如下:
接下来考虑何时在读者线程中进行解锁操作。根据要求(3):在读者正在读,有写者等待,且新读者到来时新读者不需要等待写者,那么就需要在没有新到来的读者时等待的写者才能得到执行,所以通过判断readerCnt为0时进行解锁可以实现这一点。
综上,整个读者线程如下:
整个写者线程如下:
为了观察在读者写者读写操作过程中发生线程调度的情况,读写操作函数如下:
gettid():得到线程ID。
包含的头文件及其他定义:
2. 运行结果:
#gcc -o reader_first reader_first.c -lpthread
#./reader_first
可以看到,五个线程被创建后,从最后被创建的一个读者线程开始读数据,第一个读者线程读取一个数据后第二个第三个读者线程也进入数据区开始读操作,三个线程均读取一个数据后写者线程被调度试图进行写操作,但由于此时读者线程将互斥锁上锁,写操作不能进行,继续转到读操作,一直到三个读者线程读操作结束,readerCnt=0互斥锁被解开写者线程才继续,此时方可进行写操作。
(二)写者优先的实现
1. 写者优先情况的要求分析:
(1)一个写者不能和其他写者或读者同时访问数据集;
(2)多个读者可以同时读数据集;
(3)如果有写者等待写,读者需要等待。
与读者优先的情况相比仅最后一条要求不同。
同样写者线程写操作前后需要用&writeLock与其他读者写者互斥,但考虑要求(3)在有写者等待时新读者需要等待写者写操作完成后才能读,那么可以对等待的写者通过writerCnt进行计数,在有写者到来时writerCnt+1,然后在第一个写者到来时用pthread_mutex_lock(&readerLock) 给读者上锁,在写操作结束,退出写线程之前对writerCnt-1,直到所有写线程退出,writerCnt=0后才对读者解锁。
综上写者线程代码如下:
此时的读者线程,显然在原来的基础上要执行读操作还需要获得&readerLock互斥锁,但&readerLock解锁操作不能放在read()操作之后,否则会造成读者不能并发读操作,代码变为如下情况:
但此时,可能有成千上万的读者被阻塞在 pthread_mutex_lock(&readerLock)处,若在写者线程全部退出并释放readLock时,又有新的写者到来,将导致该写者和成千上万个读者共同竞争该互斥锁,这将很可能导致新到来的写者线程的饥饿,与写者优先的原则相违背,为了解决这一问题,可以在读者线程中再添加一把互斥锁outerLock,结构变为下面情况:
此时,将只有一个读者被阻塞在 pthread_mutex_lock(&readerLock)处,而后面的所有读者均被阻塞在 pthread_mutex_lock(&outerLock)处,当写者或写者释放readerLock后,写者将优先获得互斥锁&readerLock执行写操作。
为了模拟新读者到来的情况,将main函数改为如下,read()和write()函数同上。
其他定义:
2. 运行结果
#gcc -o writer_first writer_first.c -lpthread
#./writer_first
可见,开始创建了一个读者两个写者后,2141号写者最先开始写,写入一个数据后2140号写者尝试进行写操作但由于writeLock的存在使得写操作不能并发,同样,后续的的读者尝试进行读操作也不能成功,直到2141号写者write finished退出线程后,下一个写者2140号才可以进行写操作,最后该写线程结束退出后,读线程中的读操作才得到执行。