一、实验目的
1、回顾操作系统进程、线程的有关概念,加深对Windows线程的理解;
2、了解互斥体对象,利用互斥与同步操作编写生产者-消费者问题的并发程序,加深对P (即semWait)、V(即semSignal)原语以及利用P、V原语进行进程间同步与互斥操作的理解。
二、流程图
三、代码实现
#include <windows.h>
#include <iostream>
const unsigned short SIZE_OF_BUFFER = 2; //缓冲区长度
unsigned short ProductID = 0; //产品号
unsigned short ConsumeID = 0; //将被消耗的产品号
unsigned short in = 0; //产品进缓冲区时的缓冲区下标
unsigned short out = 0; //产品出缓冲区时的缓冲区下标
int buffer[SIZE_OF_BUFFER]; //缓冲区是个循环队列
bool p_ccontinue = true; //控制程序结束
HANDLE Mutex; //用于线程间的互斥
HANDLE FullSemaphore; //当缓冲区满时迫使生产者等待
HANDLE EmptySemaphore; //当缓冲区空时迫使消费者等待
DWORD WINAPI Producer(LPVOID); //生产者线程
DWORD WINAPI Consumer(LPVOID); //消费者线程
int main()
{
//创建各个互斥信号
//注意,互斥信号量和同步信号量的定义方法不同,互斥信号量调用的是CreateMutex函数
//同步信号量调用的是 CreateSemaphore 函数,函数的返回值都是句柄。
Mutex = CreateMutex(NULL, FALSE, NULL);
EmptySemaphore = CreateSemaphore(NULL, SIZE_OF_BUFFER, SIZE_OF_BUFFER, NULL);
//EmptySemaphore = CreateSemaphore(NULL, 0, SIZE_OF_BUFFER-1, NULL);
//(第二次修改)
FullSemaphore = CreateSemaphore(NULL, 0, SIZE_OF_BUFFER, NULL);
//调整下面的数值,可以发现,当生产者个数多于消费者个数时,
//生产速度快,生产者经常等待消费者;反之,消费者经常等待
const unsigned short PRODUCERS_COUNT = 3; //生产者的个数
const unsigned short CONSUMERS_COUNT = 1; //消费者的个数
//const unsigned short PRODUCERS_COUNT = 1; //生产者的个数(第一次修改)
//const unsigned short CONSUMERS_COUNT = 3; //消费者的个数(第一次修改)
//总的线程数
const unsigned short THREADS_COUNT = PRODUCERS_COUNT + CONSUMERS_COUNT;
HANDLE hThreads[THREADS_COUNT]; //各线程的handle
DWORD producerID[PRODUCERS_COUNT]; //生产者线程的标识符
DWORD consumerID[CONSUMERS_COUNT]; //消费者线程的标识符
//创建生产者线程
for (int i = 0;i < PRODUCERS_COUNT;++i) {
hThreads[i] = CreateThread(NULL, 0, Producer, NULL, 0, &producerID[i]);
if (hThreads[i] == NULL)
return -1;
}
//创建消费者线程
for (int i = 0;i < CONSUMERS_COUNT;++i) {
hThreads[PRODUCERS_COUNT + i] = CreateThread(NULL, 0, Consumer, NULL, 0, &consumerID[i]);
if (hThreads[i] == NULL)
return -1;
}
while (p_ccontinue) {
if (getchar()) { //按回车后终止程序运行
p_ccontinue = false;
}
}
return 0;
}
void Produce() //生产一个产品。简单模拟了一下,仅输出新产品的 ID 号
{
std::cout << std::endl << "Producing " << ++ProductID << " ... ";
std::cout << "Succeed" << std::endl;
}
void Append() //把新生产的产品放入缓冲区
{
std::cerr << "Appending a product ... ";
buffer[in] = ProductID;
in = (in + 1) % SIZE_OF_BUFFER;
std::cerr << "Succeed" << std::endl;
//输出缓冲区当前的状态
for (int i = 0;i < SIZE_OF_BUFFER;++i) {
std::cout << i << ": " << buffer[i];
if (i == in) std::cout << " <-- 生产";
if (i == out) std::cout << " <-- 消费";
std::cout << std::endl;
}
}
void Take() //从缓冲区中取出一个产品
{
std::cerr << "Taking a product ... ";
ConsumeID = buffer[out];
buffer[out] = 0;
out = (out + 1) % SIZE_OF_BUFFER;
std::cerr << "Succeed" << std::endl;
//输出缓冲区当前的状态
for (int i = 0;i < SIZE_OF_BUFFER;++i) {
std::cout << i << ": " << buffer[i];
if (i == in) std::cout << " <-- 生产";
if (i == out) std::cout << " <-- 消费";
std::cout << std::endl;
}
}
void Consume() //消耗一个产品
{
std::cout << "Consuming " << ConsumeID << " ... ";
std::cout << "Succeed" << std::endl;
}
DWORD WINAPI Producer(LPVOID lpPara) //生产者
{
while (p_ccontinue) {
WaitForSingleObject(EmptySemaphore, INFINITE); //p(empty);
WaitForSingleObject(Mutex, INFINITE); //p(mutex);
Produce();
Append();
Sleep(1500);
ReleaseMutex(Mutex); //V(mutex);
ReleaseSemaphore(FullSemaphore, 1, NULL); //V(full);
}
return 0;
}
DWORD WINAPI Consumer(LPVOID lpPara) //消费者
{
while (p_ccontinue) {
WaitForSingleObject(FullSemaphore, INFINITE); //P(full);
WaitForSingleObject(Mutex, INFINITE); //P(mutex);
Take();
Consume();
Sleep(1500);
ReleaseMutex(Mutex); //V(mutex);
ReleaseSemaphore(EmptySemaphore, 1, NULL); //V(empty);
}
return 0;
}
四、实验结果与分析
1、修改前
设置的缓冲区大小为2,初始情况下生产者和消费者共享空的缓冲区(in指针和out指针都指向0号缓冲区)。当1号生产者生产出1号产品之后,将产品号存入0号缓冲区,in指针向后移动一位,指向1号缓冲区。之后2号生产者再次生产2号产品后,将产品号存入1号缓冲区,in指针向后移动一位,由于缓冲区采取循环队列的数据结构,因此下一次in指针将指向0号缓冲区,此时缓冲区已满,因此3号及之后的生产者想要生产只能原地等待消费者去消费产品。此时1号消费者消费1号产品之后,将0号缓冲区清空,out指针向后移动一位,指向1号缓冲区。之后2号消费者继续消费2号产品,将1号缓冲区清空,out指针向后移动一位,指向0号缓冲区,此时缓冲区已空,因此3号及之后的消费者想要消费只能原地等待生产者去生产产品。以此类推可以分析出所有的结果。
2、第一次修改
调整PRODUCERS_COUNT和CONSUMERS_COUNT的值可以发现,当消费者数目大于生产者时,消费速度快,消费者经常等待生产者。
3、第二次修改
将信号量修改后结果无输出。初始状态下buff空的信号量应该是等于buffer的大小的,若按题目要求修改后,buffer空信号量初始化变为0,这就说明buffer一开始是满的,但是实际上buffer却应该是空的。所以生产者P(EmptySemaphore)时就会一直被阻塞,而此时因为生产者没生产,所以消费者P(FullSemaphore)时也会一直被阻塞,从而形成死锁而无法输出。
五、问题讨论
1、仔细阅读源程序,找出创建线程的WINDOWS API函数,回答下列问题:说明线程的第一个执行函数是什么(从哪里开始执行)?它位于创建线程的API函数的第几个参数中?
答:线程的第一个执行函数是创建线程的函数CreateThread(),从Producer()/Consumer()开始执行;位于创建线程的API函数的第三个参数中。
2、第一次修改代码时调整PRODUCERS_COUNT和CONSUMERS_COUNT的值,使得消费者数目大于生产者,再重新编译运行程序,列出输出结果,能从中得出什么结论?
答:调整PRODUCERS_COUNT和CONSUMERS_COUNT的值可以发现,当消费者数目大于生产者时,消费速度快,消费者经常等待生产者。
3、第二次修改代码时按照实验说明修改信号量EmptySemaphore的初始化方法,再重新编译运行程序,列出输出结果并观察与上一次有何不同?
答:将信号量如上修改,结果无输出。初始状态下buff空的信号量应该是等于buffer的大小的,若按题目要求修改后,buffer空信号量初始化变为0,这就说明buffer一开始是满的,但是实际上buffer却应该是空的。所以生产者P(EmptySemaphore)时就会一直被阻塞,而此时因为生产者没生产,所以消费者P(FullSemaphore)时也会一直被阻塞,从而形成死锁而无法输出。
4、根据实验结果并查看MSDN(Microsoft Developer Network),回答下列问题:
① CreateMutex中有几个参数?各代表什么含义?
答:CreateMutex共含有三个参数:
(ⅰ)lpMutexAttributes SECURITY_ATTRIBUTES:表示使用不允许继承的默认描述符;
(ⅱ)bInitialOwner BOOL:希望进程立即拥有互斥体,则设为TRUE;
(ⅲ)lpName String:指定互斥体对象的名字;
② CreateSemaphore中有几个参数?各代表什么含义?信号量的初值在第几个参数中?
答:CreateSemaphore共含有四个参数:
(ⅰ)lpSemaphoreAttributes SECURITY_ATTRIBUTES:定义了信号机的安全特性;
(ⅱ)lInitialCount Long:设置信号机的初始计数;
(ⅲ)lMaximumCount Long:设置信号机的最大计数;
(ⅳ)lpName String:指定信号机对象的名称。
信号量的初值在第2个参数中。
③ 程序中P、V原语对应的实际Windows API函数是什么?写出这几条语句。
答:(ⅰ)WaitForSingleObject(FullSemaphore,INFINITE); //P(full)
(ⅱ)WaitForSingleObject(Mutex,INFINITE); //P(mutex)
(ⅲ)ReleaseMutex(Mutex); //V(mutex)
(ⅳ)ReleaseSemaphore(EmptySemaphore,1,NULL); //V(empty)
④ CreateMutex 能用CreateSemaphore替代吗?尝试修改程序,将信号量Mutex完全用CreateSemaphore及相关函数实现,并写出要修改的语句。
答:可以,因为互斥信号量本质上是二元信号量,因此只需要修改初始化代码和V操作代码即可,修改如下:
Mutex = CreateSemaphore(NULL, 1, 1, NULL);
ReleaseSemaphore(Mutex, 1, NULL); //V(mutex)