什么是消费者与生产者模型
消费者与生产者模型是通过一个容器来解决两者的前耦合问题 , 生产者生产出一个产品之后,将其放到这个容器中, 消费者消费的时候在从容器中拿到这个产品, 这个容器就相当与一个缓冲区
但是生产者生产数据和消费者消费数据是不能同时进行的, 也就是说生产者放的时候,消费者不能从中拿;消费者拿的时候,生产者不能放----这就是一中互斥的关系
而且,当缓冲区空的时候,消费者需要等待生产者放入数据后在取,而当缓冲区满的时候,生产者需要等待消费者将数据拿走之后在放入数据----这就是一种同步的关系
总结来说就是:一个场所,两个角色,三种关系
- 生产者与生产者之间是互斥关系
- 消费者与消费者之间是同步关系
- 生产者与消费者之间是同步与互斥的关系
消费者与生产者模型的优点在于解耦合,支持并发,支持忙闲不均
实现消费者与生产者模型
在实现生产者与消费者模型的过程中, 我们需要实现一个容器, 这个容器我们长称为BlockingQueue
在这个对列中,我们要保证线程安全的出队与入队操作,因此要用到互斥锁与条件变量
#define MAXQ 10
class BlockingQueue{
public:
//构造函数
BlockingQueue(int maxq=MAXQ)
:_capacity(maxq)
{
pthread_mutex_init(&_mutex,NULL);
pthread_cond_init(&_con_cond,NULL);
pthread_cond_onot(&_pro_cond,NULL);
}
//析构函数
~BlockingQueue(){
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_con_cond);
pthread_cond_destroy(&_pro_cond);
}
//入队操作
bool QueuePush(int data){
pthread_mutex_lock(&_mutex);
while(_queue.size()==_capacity){
pthread_cond_wait(&_pro_cond,&_mutex);
}
_queue.push(data);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_con_cond);
return true;
}
//出队操作
bool QueuePop(int& data){
pthread_mutex_lock(&_mutex);
while(_queue.empty()){
pthread_cond_wait(&_pro_cond);
}
data=_queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_con_cond);
return true;
}
private:
pthread_mutex_t _mutex;
//消费者与生产者条件变量
pthread_cond_t _con_cond;
pthread_cond_t _pro_cond;
vector<int> _queue;
int _capacity;
};
接下来创建线程,让多个执行流去验证生产者与消费者模型
void thr_pro(void* arg){
BlockingQueue* q=(BlockingQueue*)arg;
int data=0;
while(1){
q->QueuePush(data);
cout<<"生产者生产了一个data"<<data++<<endl;
}
return NULL;
}
void thr_con(void* arg){
BlockingQueue* q=(BlockingQueue*)arg;
int data;
while(1){
q->QueuePop(data);
cout<<"消费者消费了一个data"<<data<<endl;
}
return NULL;
}
#define NUM 5
int main(){
BlockingQueue q;
pthread_t con_tid[NUM],pro_tid[NUM];
for(int i=0;i<NUM;i++){
int ret=pthread_create(&con_tid[i],NULL,thr_con,(void*)&q);
if(ret!=0){
cout<<"创建线程失败!!"<<endl;
return -1'
}
}
for(int i=0;i<NUM;i++){
int ret=pthread_create(&pro_tid[i],NULL,thr_pro,(void*)&q);
if(ret!=0){
cout<<"创建线程失败!!"<<endl;
return -1'
}
}
for(int i=0;i<NUM;i++){
pthread_join(con_tid[i],NULL);
pthread_join(pro_tid[i],NULL);
}
return 0;
}
注意 :
在实现BlokingQueue类时, 我们给出了队列的最大容量为符 10,也就是队列中只能存放10个产品,那么当队列为空的时候,就应该阻塞消费者,当队列满的时候就应该阻塞生产者.
信号量
在Linux下有system V 和 posix 两套标准的信号量, system V标准的信号量在Linux下作为系统调用接口 , 而posix标准的信号量为库函数, 因此他们的移植性较好.
posix标准信号量
信号量是通过内核中的计数器来实现同步与互斥
posix信号量既可以实现线程间的同步与互斥,也可以实现线程间的同步与互斥, 但是在线程以进程间的实现原理是不同的, 在线程间,是通过一个全局的计数器实现, 而在进程间是通过共享内存中的计数器实现
他的实现原理是通过等待+唤醒+等待队列 , 但是与条件变量略有不同
- 条件变量 : 等待+唤醒+等待队列-------条件需要用户自己进行外部判断,并且搭配互斥锁一起使用
- 信号量 : 通过自身的计数实现条件的判断 , 不需要搭配互斥锁(自身计数保证原子操作)
同步的实现
信号量通过一个计数器实现对资源的计数, 并且通过这个计数器来判断当前进程或线程能否对临界资源进行访问 , (对临界资源进行访问之前,先访问信号量判断能够进行访问)
若资源计数 > 0 ; 则可以进行访问 调用直接返回, 并且计数减一
若计数<=0, 表示你没有资源, 无法访问, 调用阻塞,线程进入等待队列
若其他线程促使生产满足,则发起调用 , 资源计数+1 ; 唤醒等待队列上的线程
互斥的实现
计数只有 0/1 ;用于标记两种状态,(资源只有一个, 同一时间只有一个线程能进行访问) ; 对临界资源访问之先判断计数 , 若>0;则计数-1(相当于互斥锁中的加锁操作),并且调用返回, 对临界资源进行访问 , 访问完毕, 在对计数器进行+1(相当于互斥锁中的解锁操作), 唤醒所有线程, 重新开始对计数进行判断
接口
- 信号量的初始化
//定义一个信号量
int sem_t;
//销毁信号量
int sem_destroy(sem_t *sem);
int sem_init(sem_t *sem, int pshared, unsigned int value);
- sem : 要初始化的信号量
- pshared : 表示该信号量适用于线程间还是用于进程间, 0表示为线程间, 非0 表示用于进程间
- value : 设置计数器的初值.
- 等待信号量
int sem_wait(sem_t *sem);
//尝试等待
int sem_trywait(sem_t *sem);
//限时等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
在sem_wait接口中,会进行一个计数-1 , 在判断是否有资源, 没有则会阻塞,有资源则返回
sem_trywait接口是一个非阻塞的判断等待, 如果没有资源则立即报错返回
- 唤醒
int sem_post(sem_t *sem);
在sem_post接口中, 会进行一个计数+1, 并且唤醒等待的线程
使用信号量来实现消费者与生产者模型
使用vector来完成一个唤醒的队列,在vector中定义一个read与write标记位, 每次在唤醒队列中放数据时, 放在下标为write的位置, 读取数据时读下标为read的数据
#define MAXQ 10
class RingQueue{
public:
//构造函数
RingQueue(int maxq=MAXQ)
:_capacity(maxq)
,_sub_read(0)
,_sub_write(0)
,_queue(maxq)
{
sem_init(&_data_num,0,0);
sem_init(&_idle_num,0,_capacity);
sem_init(&_lock,0,1);
}
//析构函数
~RingQueue(){
sem_destroy(&_lock);
sem_destroy(&_data_num);
sem_destroy(&_idle_num);
}
//入队操作
bool QueuePush(int data){
sem_wait(&_idle_num);
sem_wait(&_lock);
_queue[_sub_write]=data;
//环形队列指针向后移动
_sub_writ=(_sub_write+1)%_capacity;
sem_post(&_lock);
sem_post(&_data_num);
return true;
}
//出队操作
bool QueuePop(int& data){
sem_wait(&_data_number);
sem_wait(&_lock);
data=_queue[_sub_read];
_sub_read=(_sub_read+1)%_capacity;
sem_post(&_lock);
sem_post(&_idle_num);
return true;
}
private:
vector<int> _queue;
//表示读取数据的标识
int _sub_read;
//表示放入数据的标识
int _sub_write;
sem_t _lock;
//表示
sem_t _data_num;
sem_t _idle_num;
int _capacity;
};
注意 :
在RingQueue类中, 出队与入队的操作不需要进行队列的满或者空的判断, 因为直接调用sem_wait接口是,接口内部就会进行判断,若队列为空时, 出队操作就会阻塞在data_num的等待队列中 ; 当对列满时, 入队操作就会阻塞在idle_num的等待队列上.
void thr_con(RingQueue* q){
int data=0;
while(1){
q->QueuePop(data);
cout<<"消费者消费了一个产品------"<<data<<endl;
}
return NULL;
}
void thr_pro(RingQueue* q){
int data=0;
while(1){
q->QueuePush(data);
cout<<"生产者生产了一个产品------"<<data<++<endl;
}
return NULL;
}
int main(){
RingQueeu q;
vector<thread> con(4);
vector<thread> pro(4);
for(int i=0;i<4;i++){
con[i]=thread(thr_con,&q);
}
for(int i=0;i<4;i++){
pro[i]=thread(thr_pro,&q);
}
for(int i=0;i<4;i++){
con[i].join();
pro[i].ioin();
}
return 0;
}