1. 问题描述及需求分析
以生产者-消费者模型为基础,在 Windows 环境下创建一个控制台进程
(或者界面进程),在该进程中创建读者写者线程模拟生产者和消费者。写
者线程写入数据,然后将数据放置在一个空缓冲区中供读者线程读取。读者
线程从缓冲区中获得数据,然后释放缓冲区。当写者线程写入数据时,如果
没有空缓冲区可用,那么写者线程必须等待读者线程释放出一个空缓冲区。
当读者线程读取数据时,如果没有满的缓冲区,那么读入线程将被阻塞,直
到新的数据被写进去。
2. 实验设计
缓冲区是临界资源,不论是生产者还是消费者访问临界资源的时候都需
要互斥的访问。 对于访问临界资源必须有个互斥信号量 mutex,其初始值为
1,表示可以访问。 对于临界资源的访问不分生产者还是消费者,都是一个
进程访问临界资源的时候其他进程需要等待。
生产者与消费者是互相合作的关系,我们为完成某种任务而建立多个进
程,这些进程因为要在某些位置上协调他们的工作次序而等待,比如说 A 进
程要工作必须等待 B 进程的一个结果,如果仅仅是 A 进程单方面的需要 B 进
程的一个结果,那这张制约关系就是 A 制约 B 的,如果同时 B 进程的工作也
需要 A 进程工作的结果,那么就是 A 与 B 互相制约了。
生产者-消费者问题里的同步关系我认为是 A 与 B 互相制约的情况,因为
生产者要生产的前提是缓冲区没满,而缓冲区没满是消费者运行后的结果,
同样消费者要运行的前提是缓冲区不空,而缓冲区不空是生产者不断生成的
结果。本题的同步关系需要两个信号量。一个是消费者通知生产者是否可以
生产的“缓冲区空”信号量 empty,一个是生产者通知消费者要消费的“缓
冲区满”信号量 full。
3. 实验代码
#include
#include
#include
#include
using namespace std;
#define STD __stdcall
#define LENGTH 20
#define
GETMYRAND()
(int)(((double)rand()/(double)RAND_MAX)*300)
//使用临界区来同步线程
CRITICAL_SECTION _cr;
//空信号量
HANDLE emptySemaphore = NULL;
//满信号量
HANDLE fullSemaphore = NULL;
//缓冲区 Buffer
vector buffer;
//计划生产数
int planNum=20;
//实际生产数
int num = 0;
// 消费者线程
DWORD STD Consumer(void* lp) {
while(true) {
//等待判断缓冲区满的信号量
WaitForSingleObject(fullSemaphore,0xFFFFFFFF);
//进入临界区,线程同步,功能同互斥量
EnterCriticalSection(&_cr);
//消费者线程从缓冲区中取出消费一个资源
buffer.pop_back();
//打印当前缓冲区可用资源数
cout << "消费者消费资源,缓冲区资源:" << buffer.size()
<< endl;
//离开临界区
LeaveCriticalSection(&_cr);
//释放判断缓冲区空的信号量
ReleaseSemaphore(emptySemaphore,1,NULL);
//线程睡眠随机时间
Sleep(GETMYRAND());
}
return 0;
}
// 生产者线程
DWORD STD Producer(void* lp) {
while(num < planNum){
//等待判断缓冲区空的信号量
WaitForSingleObject(emptySemaphore, 0xFFFFFFFF);
//进入临界区,线程同步,功能同互斥量
EnterCriticalSection(&_cr);
//生产者线程向缓冲区中生成一个资源
buffer.push_back(1);
num++;
//打印当前缓冲区可用资源数
cout << "生产者生产资源,缓冲区资源:" << buffer.size()
<< endl;
//离开临界区
LeaveCriticalSection(&_cr);
//释放判断缓冲区满的信号量
ReleaseSemaphore(fullSemaphore, 1, NULL);
//线程睡眠随机时间
Sleep(GETMYRAND());
if(num == planNum){
cout << "\n 生产者已经完成计划生产数:" << num <<"\n"<<
endl;
}
}
}
return 0;
int main() {
//创建信号量
emptySemaphore = CreateSemaphore(NULL, LENGTH, LENGTH, NULL);
fullSemaphore = CreateSemaphore(NULL, 0, LENGTH, NULL);
//初始化临界区
InitializeCriticalSection(&_cr);
//开启多线程
HANDLE handles[3];
handles[2] = CreateThread(0, 0, &Producer, 0, 0, 0);
handles[1] = CreateThread(0, 0, &Producer, 0, 0, 0);
handles[0] = CreateThread(0, 0, &Consumer,
0, 0, 0);
//等待子线程执行完毕
WaitForMultipleObjects(3, handles, true, INFINITE); //"Join"
trreads
//释放子线程
CloseHandle(handles[0]);
CloseHandle(handles[1]);
CloseHandle(handles[2]);
//释放临界区
DeleteCriticalSection(&_cr);
return 0;
}
4. 实验结果与分析
实验过程:
1、首先创建空、满缓冲区信号量
2、初始化临界区(P(mutex)和 V(mutex)之间)
3、创建生产者,消费者子线程(2 个生产者,1 个消费者)
4、在判断缓冲区为空时向缓冲区存入商品
5、在判断缓冲区为满时从缓冲区中拿出商品
6、每一轮执行存入或拿出商品都显示缓冲区资源数
7、释放生产者、消费者子线程
8、释放临界区
因为有多个线程的生产者和消费者,所以第一个生产者发现生产数目达
到计划生产数后,其他生产者还会继续判断缓冲区是否已经满的信息但是发
现总生产数已经达到计划生产数,就再次显示出生产者已经完成计划生产数
的提示。
程序中的 P(mutex)和 V(mutex)必须成对出现,夹在两者之间的代码段是
临界区;施加于信号量 empty 和 full 上的 PV 操作也必须成对出现,但分别
位于不同的程序中。在这个问题中,P 操作的次序是很重要的,如果把生产
者进程中的两个 P 操作交换次序,那么,当缓冲区中存满 k 件产品(empty=0,
mutex=1,full=k)时,生产者又产生一件产品,在它预想缓冲区存放时,将
在 P(mutex)上等待,由于此时 mutex=0,它已经占有缓冲区,这是消费者欲
取产品将停留在 P(mutex)上而得不到使用缓冲区的权力。这就导致生产者永
远等待消费者取走产品,而消费者却在等待生产者释放缓冲区过得占有权,
这种相互之间的等待永远不可能结束。所以,在使用信号量和 PV 操作实现进
程同步时,要特别当心 P 操作的次序,而 V 操作的次序无关紧要。一般来说,
用于互斥的信号量上的 P 操作总在后面执行。
5. 实验心得
通过本次实验,我对生产者-消费者问题的相关知识有了更加深刻的理解,
进一步掌握了进程的同步的相关概念,理解了利用信号量机制解决进程同步
问题的基本方法。队列和链表在 C++中是可以直接用已经有的模板的。消费
者要是能放到缓冲区里就看生产者等待队列里有没有人,有人就唤醒一个生
产者。要是缓冲区只有 1 那么大,最终完成链表中必定是生产者和消费者交
替,这是一个验证程序对错的方法。可以定义两个互斥信号量 mutex1 和
mutex2 来代替 mutex,他们分别用于多个生产者和多个消费者工作时的各自
互斥,这样可以使生产者-消费者问题的并发性进一步提高。