本章目标
1.生产消费者模型概述
2.基于阻塞队列的生产消费者模型
2.基于环形队列信号量的生产消费模型
1.生产消费者模型概述
我们的线程同步与互斥,主要是在多线程当中,用来处理多线程调度的问题.而我们线程创建出来和进程的目标是一致的都是完成任务.但是在线程中难免会遇到生产数据和消费数据是混杂的.为了解决这个问题.我们将生产消费这种强耦合的问题,通过一种容器将其分为两个部分进行解耦.我们就称之为生产消费者模型,因为我们将生产和消费分为了两个部分.我们也就能够让生产消费同时进行.也就是生产消费者模型是能够支持并发的

对于生产消费者模型.我们主要关注的是三个部分
1.我们要维持生产者和生产者之间的关系,消费者与消费者之间的关系,生产和消费者之间的关系
2.要有2种角色,生产者,消费者
3.一个消费场所---也就是我们的容器
2.基于阻塞队列的生产消费者模型
我们既然要做一个生产消费者模型,我们这个方案是选用阻塞队列的版本.最后存储数据的容器是队列.
我们既然要实现的我们的生产消费者模型,我们用两个不同的条件变量去等待生产者和消费者.这里主要是维持了生产者和生产者之间的关系,消费者与消费者之间的关系.我们用一把锁去维持生产者与消费者之间的关系.
cpp
int _cap;
std::queue<T> block_queue; //阻塞队列
//保证线程互斥
//拓展方案: 双锁提高并行数量,双条件变量精准唤醒,原子变量保证唤醒条件
//单锁方案,天生保证生产并发,和消费并发,只需要保证生产和消费的并发即可
Mutex_Moudle::Mutex lock;
Cond_Module::Cond ccond; //消费者条件变量
Cond_Module::Cond pcond; //生产者条件变量
//唤醒策略
int cum_pthread_num;
int pro_pthread_num;
最后我们的成员变量如上.同时定义_cap来表示当前队列当中数据的个数.
cum_pthread_num,和pro_pthread_num来表示在条件变量的等待的线程的个数
cpp
void Enqueue(T data)
{
//生产者
{
Mutex_Moudle::Mutex_Grard guard(lock);
while(block_queue.size()==_cap)//防止伪唤醒,过量唤醒
{
pro_pthread_num++;
pcond.wait(lock);
pro_pthread_num--;
}
//一定是有位置的
block_queue.push(data);
//唤醒消费者
if(cum_pthread_num>0)
ccond.signal();
}
}
这里我们进入Enqueue的生产者线程进来要先竞争锁,这个是所有生产者线程和消费者线程一起来竞争这把锁.来决定是那个线程来继续实现下面的行为.最主要的是这把锁是为了维持生产者和消费者之间的关系.
加完锁之后回去判断,当前队列当中,是否数据已经满了.
如果满了就让生产者去生产者的条件变量上等候,因为数据已经太多了.此时的生产者线程再来也无法生产数据.
接下来有两种情况,第一种,是再上面的循环没触发直接下来的.这种属于,生产和消费者维持着一种你生产,我们消费并没有过量的关系.
而第二种,就是从条件变量上醒来.这种是之前数据已经满了.而消费者把数据消费完已经有空位了然后下来生产数据.
当将数据生产之后,我们就可以唤醒一个,在消费者条件变量上等待的线程过来消费数据.这里如果在条件变量上没有任何线程的化,也不会有任何问题.
这里面,我们条件变量判断主要采用了循环判断的方法.这么做最主要是为了防止伪唤醒,因为我们的线程可能有很多种被唤醒的情况.可是在当前线程的条件变量的条件并没有满足就被放出来了.
可能这个伪唤醒,是因为信号的方式导致在条件变量的上的线程被提前激活了.为了防止这种情况.我们采用了while循环的方式来进行判断.
cpp
void pop(T* data)
{
//消费者
{
Mutex_Moudle::Mutex_Grard gurad(lock);
while(block_queue.empty())
{
cum_pthread_num++;
ccond.wait(lock);
cum_pthread_num--;
}
*data = block_queue.front();
block_queue.pop();
//唤醒生产者
if(pro_pthread_num>0)
pcond.signal();
}
}
消费者的代码有异曲同工之妙,但是在条件变量的循环上判断的条件从当前队列当中的元素从全满变为阻塞队列为空.
阻塞队列的生产消费者模型的具体代码实现
篇幅原因,我们整体采用都是我们之前自己封装出来的组件去实现这个阻塞队列.但是在博客当中,我们只说最重要的部分.
2.基于环形队列信号量的生产消费模型
2.1信号量
在IPC阶段我们曾经用systemV标准下的信号量集实现了一个由建造者模式的信号量的封装.
而我们今天的所说的,是posix标准下的信号量.它与systemV标准信号量不同点虽然由很多.但是核心原理是一致的
它解决我们之前system V标准下的信号量集使用困难,创建和初始化并非原子的问题.
同时它接口设计的非常简单易懂.具体原理,我们就不介绍了.但是信号量是可以自身就能够实现线程的同步与互斥的关系的.

信号量初始化,所有的信号量的接口都在semaphore.h这个头文件当中,但是他也是posix原生线程库的一部分.同样需要链接pthread这个库
它的第一个参数就是信号量,第二个是否是在进程间能够使用,我们直接给0,第三个就是这个信号量的初始值.
这个地方就与system V表准的那个信号量不同.它的初始化需要再设置.而创建它的那个value表示当前信号集当中的信号量的个数sem_ght

上面是我们的system V的创建信号量集的方法

这个是posix标准下的P原语,也就是--的那个

这个是V原语++的那个.
这两个接口只需要将对应的信号量传进来即可

对于信号量来说它也需要进行销毁
cpp
#pragma once
#include <semaphore.h>
#include <pthread.h>
namespace Sem_Module
{
class Sem
{
public:
Sem(int init_val)
{
// 第二个参数表示是否为进程还是是否为线程
if (init_val >= 0)
{
int n = sem_init(&sem, 0, init_val);
(void)n;
}
}
void P()
{
//--
int n = sem_wait(&sem);
(void)n;
}
void V()
{
//++
int n = sem_post(&sem);
(void)n;
}
~Sem()
{
sem_destroy(&sem);
}
private:
sem_t sem;
};
}
上面就是我们对信号量的封装的.
2.2环形队列

对于环形队列,我们在之前也做过.它的实现方式往往是通过一个vector做实现的.
对于队列的判空和判满
第一种情况就是head==tail,同时定义个计数器,如果当前的在队列当中的值是等于我们之前设定的大小我们就认为是满的,如果=0,我们就认为这个环形队列是为空的.
对于我们的信号量来说,它本事就是一个计数器.对于消费者来说,它只需要关注当前环形队列当中是否有数据.对于生产者来说它只需要是否有格子.
同时,我们的消费者是不能够超过生产者的,同时生产者也不能够扣圈消费者.
2.3环形队列的生产消费者模型
cpp
std::vector<T> rq; // 环形队列
int _cap; // 环形队列初始值
size_t pos_producter; // 生产者位置
size_t pos_consumer; // 消费者为位置
Sem_Module::Sem data_sem; // 消费者信号量
Sem_Module::Sem black_sem; // 生产者信号量
Mutex_Moudle::Mutex lockc; // 消费者锁
Mutex_Moudle::Mutex lockp; // 生产者锁
根据上面所说,我们环形队列的生产消费者模型当中的成员变量如上.我们在这里是通过两个信号量去维持生产者与消费者之间的关系.通过两把锁去维持生产者之间和消费者之间的关系.
在这里我们给生产者信号量的值为我们环形队列的初始值,因为在环形队列最开始是没有数据的所以生产者的信号量的初始值是应该等于环形队列的初始值.
cpp
void Enqueue(const T &data)
{
// 生产者
black_sem.P();
{
lockp.Lock();
rq[pos_producter++] = data;
pos_producter %= _cap;
lockp.unLock();
}
data_sem.V();
}
对于生产者来说,我们在进行操作的时候应该先释放生产者的信号量,然后加锁,最后因为数据已经产生,所以需要让消费者的信号量++.
cpp
void Pop(T *data)
{
data_sem.P();
{
// Mutex_Moudle::Mutex_Grard guard(lockc);
lockc.Lock();
*data = rq[pos_consumer++];
pos_consumer%= _cap;
lockc.unLock();
}
black_sem.V();
}
消费者刚好与之相反,但是在这里,我们的加锁都是在申请信号量的之后加锁的.
这里是为了提高效率.因为对于信号量来说,它的申请是就是代表着资源的预定,所以我们没必要将加锁放到这之前,可以让不同的线程先抢资源,然后在进锁等待.如果我们的信号量为0的话,我们的线程会在这里直接会在这里休眠,直到信号量上有值,二者都能够完成互斥,只不过位置不同.
Linux环形队列实现代码
这里同样只介绍核心部分.具体实现在上面的链接中