一、实验题目
基于信号量机制的并发程序设计
二、实验目的
(1) 回顾操作系统进程、线程的有关概念,针对经典的同步、互斥、死锁与饥饿问题进行并发
程序设计。
(2) 了解互斥体对象,利用互斥与同步操作编写读者-写者问题的并发程序,加深对 P (即
semWait)、V(即 semSignal)原语以及利用 P、V 原语进行进程间同步与互斥操作的理解。
(3) 理解 Linux 支持的信息量机制,利用 IPC 的信号量系统调用编程实现哲学家进餐问题。
三、总体设计(含背景知识或基本原理与算法、或模块介绍、设计步骤等)
读者-写者问题:
*在读者-写者问题中,多个读者可以同时读取共享资源,但只能有一个写者可以写入共享资源。
*使用互斥体对象(mutex)实现对共享资源的互斥访问。
*使用信号量(semaphore)实现读者和写者之间的同步和互斥操作。
存在一个多进程共享的数据区,该数据区可以是一个文件或一块内存空间,甚至可以是一组寄存器,有些进程(reader)只读取这个数据区中的数据,有些进程(writer)只往数据区中写数据。此外,还必须满足:
1.任意数量的读进程可同时读这个文件。
2.一次只有一个写进程可以写文件
3.若写进程正在写文件,则禁止任何读进程读文件。
书上实现的读者优先的算法思路是:
写进程比较简单,信号量WriteSemaphore用于实施排斥,只要一个写进程正在访问数据区,其他写进程和读进程就都不能访问它。读进程也使用WriteSemaphore实施互斥,但为了允许多个读进程,没有读进程正在读时,第一个试图读的读进程需要在WriteSemaphore上等待。当至少已有一个读进程在读时,随后的读进程无须等待,可以直接进入。readcount用于记录读进程的数量,信号量XSemaphore用于确保readcount被正确地更新。
四、详细设计(含主要的数据结构、程序流程图、关键代码等)
读者-写者问题的并发程序设计
使用互斥体对象mutex来保证对共享资源的互斥访问。
使用两个信号量readers和writers来控制读者和写者之间的同步和互斥操作。
读者线程:
在进入临界区之前,使用sem_wait(&readers)来等待其他读者完成读取操作。
进入临界区读取共享资源。
使用sem_post(&readers)来释放读者信号量。
写者线程:
在进入临界区之前,使用sem_wait(&writers)来等待其他写者完成写入操作。
进入临界区写入共享资源。
使用sem_post(&writers)来释放写者信号量。
代码:
#include <windows.h>
#include <iostream>
#define MAX_READER_NUM 512
#define READER_NUM 3
#define WRITER_NUM 2
#define MOD 100
using namespace std;
int readcount = 0;
HANDLE WriteSemaphore; //实现读写互斥
HANDLE XSemaphore; //对于人数修改的互斥量
DWORD WINAPI reader(LPVOID); //读者线程
DWORD WINAPI writer(LPVOID); //写者线程
bool p_ccontinue = true; //控制程序结束
int test = 0;
int main()
{
//初始化两个信号量
/*结构体指针;信号量对象的初始计数;信号量对象的最大计数;信号量对象的名称*/
WriteSemaphore = CreateSemaphore(NULL,1,MAX_READER_NUM,NULL);
XSemaphore = CreateSemaphore(NULL,1,MAX_READER_NUM,NULL);
//用WindowsAPI模拟课本上 parbegin(reader,writer);
//总的线程数
HANDLE hThreads[READER_NUM + WRITER_NUM]; //各线程的 handle
DWORD readerID[READER_NUM]; //读者线程的标识符
DWORD writerID[WRITER_NUM]; //写者线程的标识符
//创建读者线程
for (int i=0; i<READER_NUM; i++)
{
hThreads[i]=CreateThread(NULL,0,reader,NULL,0,&readerID[i]);
if (hThreads[i]==NULL)
return -1;
}
//创建写者线程
for (int i=0; i<WRITER_NUM; i++)
{
hThreads[READER_NUM+i]=CreateThread(NULL,0,writer,NULL,0,&writerID[i]);
if (hThreads[i]==NULL)
return -1;
}
while(p_ccontinue)
{
if(getchar()) //按回车后终止程序运行
{
p_ccontinue = false;
}
}
return 0;
}
//读者阅读
void READUNIT()
{
cout<<"一个读者开始阅读:";
cout<<test<<endl;
}
//写者写
void WRITEUNIT()
{
cout<<"写者开始写:";
test = (test+1) % MOD;
cout<<test<<endl;
}
//读者
DWORD WINAPI reader(LPVOID lpPara)
{
while(p_ccontinue)
{
WaitForSingleObject(XSemaphore,INFINITE); //semWait(x);
readcount++;
if(readcount == 1) //第一个读者来了
WaitForSingleObject(WriteSemaphore,INFINITE); //semWait(wsem);
ReleaseSemaphore(XSemaphore,1,NULL); //semSignal(x);
READUNIT();
//阅读完毕
WaitForSingleObject(XSemaphore,INFINITE); //semWait(x);
readcount--;
//无读者,释放资源
if(readcount == 0)
ReleaseSemaphore(WriteSemaphore,1,NULL); //semSignal(wsem);
ReleaseSemaphore(XSemaphore,1,NULL); //semSignal(x);
Sleep(3000);
}
return 0;
}
//写者
DWORD WINAPI writer(LPVOID lpPara)
{
while(p_ccontinue)
{
WaitForSingleObject(WriteSemaphore,INFINITE); //semWait(wsem);
WRITEUNIT();
ReleaseSemaphore(WriteSemaphore,1,NULL); //semSignal(wsem);
Sleep(2000);
}
return 0;
}
五、实验结果与分析
这段代码的实验结果是模拟了读者和写者对共享资源的并发访问。由于读者和写者之间的同步和互斥操作,读者和写者会交替地对共享资源进行读取和写入操作。通过输出信息可以观察到读者和写者的行为。
值得注意的是,读者和写者之间的调度是不确定的,所以每次运行结果可能略有不同。读者和写者的执行顺序会根据操作系统的调度策略和线程的竞争情况而有所变化。
总体来说,这段代码演示了使用互斥体和信号量来实现读者-写者问题的基本思路和实现方式。通过合理地控制互斥访问和同步操作,可以确保多个读者和写者之间对共享资源的正确访问和修改。
六、小结与心得体会
在这个实验中,我们通过使用互斥体对象和信号量机制,成功设计了一个并发程序来解决读者-写者问题。通过这个实验,我得出了以下的小结和心得体会:
1.熟悉操作系统的进程和线程概念:在设计并发程序之前,我们需要对操作系统的进程和线程有一定的了解。了解进程和线程的概念、特点以及它们之间的关系,能够帮助我们更好地理解并发编程的问题和解决方法。
2.理解同步和互斥的重要性:在多个线程同时访问共享资源时,必须确保对共享资源的访问是同步和互斥的。通过使用互斥体对象和信号量,我们可以实现线程之间的同步和互斥操作,避免数据竞争和不一致的结果。
3.学会使用信号量机制:信号量是一种常用的并发编程机制,它可以用来控制并发线程之间的访问和同步。在本实验中,我们使用信号量来控制读者和写者之间的访问和修改共享资源的操作。学会使用信号量的原语,如sem_wait和sem_post,能够有效地实现线程之间的同步和互斥。
4.注意死锁和饥饿问题:在设计并发程序时,需要注意死锁和饥饿问题的可能性。死锁是指多个线程互相等待对方释放资源而无法继续执行的情况,而饥饿则是指某些线程由于资源分配不均而无法得到执行的情况。在本实验中,我们通过合理地设计互斥和同步操作,避免了死锁和饥饿的发生。
5.调试并发程序的挑战:并发程序的调试相对复杂,因为线程的执行顺序是不确定的,可能存在竞争条件和难以重现的错误。在本实验中,我们可以通过输出信息来观察并验证读者和写者的行为,以及共享资源的正确访问和修改。同时,运用调试工具和技巧可以帮助我们更好地理解程序的执行流程和发现潜在的问题。
通过完成这个实验,我对并发程序设计有了更深入的理解。同时,我也意识到在实际开发中,合理地设计并发程序是至关重要的,它涉及到线程之间的协作和资源管理。合理使用同步和互斥机制,能够提高程序的性能、可靠性和正确性,避免出现数据竞争和不一致的结果。并发编程是一项挑战,但也是一项有趣和重要的技能,对于开发高效、可扩展的应用程序至关重要。