设计并实现一个进程,该进程拥有一个生产者线程和一个消费者线程,它们使用N个不同的缓冲区(N为一个确定的数值,例如N=32)。需要使用如下信号量:
一个互斥信号量,用以阻止生产者线程和消费者线程同时操作缓冲区列表;
一个信号量,当生产者线程生产出一个物品时可以用它向消费者线程发出信号;
一个信号量,消费者线程释放出一个空缓冲区时可以用它向生产者线程发出信号。
主要程序结构
#include<iostream>
#include<Windows.h>
#include<process.h>
#include<vector>
using namespace std;
// 等价于WINAPI,约定使用stdcall函数调用,既被调用者负责清栈
#define STD __stdcall
// 确定缓冲区大小,这里选取5,便于展示和说明结果
#define LENGTH 5
// 随机时间
#define GETMYRAND() (int)(((double)rand()/(double)RAND_MAX)*300)
// 使用临界区来同步线程
CRITICAL_SECTION _cr;
//空信号量
HANDLE emptySemaphore = NULL;
//满信号量
HANDLE fullSemaphore = NULL;
// 缓冲区Buffer
vector<int> buffer;
// 消费者线程
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(true){
//等待判断缓冲区空的信号量
WaitForSingleObject(emptySemaphore, 0xFFFFFFFF);
//进入临界区,线程同步,功能同互斥量
EnterCriticalSection(&_cr);
//生产者线程向缓冲区中生成一个资源
buffer.push_back(1);
//打印当前缓冲区可用资源数
cout << "生产者新生产一个资源,当前可用资源数:" << buffer.size() << endl;
//离开临界区
LeaveCriticalSection(&_cr);
//释放判断缓冲区满的信号量
ReleaseSemaphore(fullSemaphore, 1, NULL);
//线程睡眠随机时间
Sleep(GETMYRAND());
}
return 0;
}
int main() {
//创建信号量
emptySemaphore = CreateSemaphore(NULL, LENGTH, LENGTH, NULL);
fullSemaphore = CreateSemaphore(NULL, 0, LENGTH, NULL);
//初始化临界区
InitializeCriticalSection(&_cr);
//开启多线程
HANDLE handles[2];
handles[1] = CreateThread(0, 0, &Producer, 0, 0, 0);
handles[0] = CreateThread(0, 0, &Consumer, 0, 0, 0);
//等待子线程执行完毕
WaitForMultipleObjects(2, handles, true, INFINITE); //"Join" trreads
//释放子线程
CloseHandle(handles[0]);
CloseHandle(handles[1]);
//释放临界区
DeleteCriticalSection(&_cr);
return 0;
}
一、生产者消费者线程互斥
使用critical area(临界区)来保证生产者和消费者的线程彼此互斥:
在多线程开启前后分别初始化临界区和销毁临界区。
接下来,在生产者线程函数和消费者线程函数对缓冲区进行操作的代码前后启用临界区和离开临界区,来做到两个线程间互斥同步。
最终效果是,虽然两个线程都通过死循环不断执行循环体中代码,但是在其中一个线程执行临界区代码时,另一个线程被互斥阻塞。
二、生产者、消费者分别在“满”和“空”时阻塞
生产者线程得到emptySemaphore信号量时,其计数器-1,开始生产资源。在生产结束后,使fullSemaphore的计数器+1。
消费者线程得到fullSemaphore信号量时,其计数器-1,开始生产资源。在生产结束后,使emptySemaphore的计数器+1。
通过上面所述过程,使用emptySemaphore和fullSemaphore两个信号量可以做到资源生产满时生产者阻塞,资源消费完时消费者阻塞的功能。
为了演示消费者阻塞效果,我们把生产者的生产效率降低,既在生产者线程结束后,Sleep睡眠的时间延长。
查看效果:
每隔大半秒出现一对生产者和消费者记录,说明资源不足时,已经阻塞消费者继续消费资源,而等待生产者Sleep休眠时间之后,生产出新的资源,消费者才继续消费。
为了演示生产者阻塞效果,我们把消费者的消费效率降低,既在消费者线程结束后,Sleep睡眠的时间延长。
查看效果:
当资源数到达5时,资源已满时,既阻塞生产者继续生产资源,而等待消费者Sleep休眠时间之后,消费掉资源,生产者才继续生产。
查阅资料及笔记:
在生产者和消费者的代码中经常出现DWORD WINAPI
其中,WINAPI是宏定义 #define WINAPI __stdcall
stdcall是一种函数调用约定,其清栈工作由被调用者执行,对于节省内存效果很好,但缺点是参数不是可变长类型。
另外还有__cdcel、__fastcall等。
临界区、互斥量、信号量、事件总结:
1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
临界区的缺点是:没有办法知道进入临界区中的那个线程是生是死。如果那个线程在进入临界区后当掉了,而且没有退出来,那么系统就没有办法消除掉此临界区。
2. 互斥量(Mutex),信号量(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和 线程退出。
3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。
mutex和samephore都是信号量,但前者实现critical area的功能,后者带计数器。mutex可以说是计数为1的samephore。