Linux——生产者消费者模型
文章目录
一、生产者消费者模型引入
生产者消费者模型就是好比是一个超市,超市作为货物存放的载体,而生产者将商品上架,消费者则按需购买商品
但这个结构能运行起来需要一定的规则,作为一个超市,肯定不止一个消费者和一个生产者,同一个商品有可能是不同的生产厂商,不同的消费者可能想买相同的商品,超市内同一个位置只能放一件商品,一件商品只能被一个消费者购买,还有消费的前提是货架上有生产者放的货品等等规则,要满足上述条件,需要搞清楚生产者与消费者之间的关系
生产者与消费者之间的三种关系:
生产者之间是什么关系? 竞争—互斥
消费者和消费者之间? 竞争—互斥
生产和消费之间? 互斥&&同步
总结:
321原则:
3:3种关系
2:2种角色,生产者(1 or n),消费者(1 or n) ——线程或者进程
1:一个交易场所,内存空间
如何理解CP问题:
生产者消费者模型将生产与消费的过程进行了解耦,也就是可以边生产边消费,而不是先生产后消费,内存空间作为一个载体相当于一个仓库,暂存着数据,支持忙闲不均,可以提高数据处理的效率,因为真正在实际中耗时间的并不是拿放数据,而是生产和处理数据的过程,拿或取数据只是一瞬间的事情,数据被拿走了就不属于临界区的共享资源了,是线程自己的事情了,多线程拿走数据就能同时进行不同数据的处理,提高并发,从而提高了效率
二、基于BlockingQueue的生产者消费者模型
2.1 条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中,这种情
况就需要用到条件变量
理解条件变量:
假设线程A是苹果生产商,线程B与C恰好都需要买苹果,但此时恰好商店里的苹果断货了,需要等待线程A来补货,由于苹果是公共资源,我们知道多线程访问公共资源需要保持互斥,所以由于线程B先来到商店,所以B先申请了锁,但B发现苹果没货,又只能又释放了锁,可是B并不知道什么时候苹果有货,只能一直申请锁,访问资源发现没货,然后释放锁,如此反复进行,导致线程C并没有机会访问共享资源,C就产生了饥饿问题,因为B一直在毫无意义的申请共享资源,这就导致了资源的浪费
条件变量就像一个超市里的铃铛一样,当线程B与C在访问资源发现没货的时候就不再一直申请锁和释放锁了,而是在铃铛下排队,当有货的时候铃铛就会响,也就是唤醒B和C线程,进行消费
条件变量的作用:
- 不做无效的锁申请
- 执行具有顺序
条件变量的本质:
条件变量的接口:
初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,也就是锁,这里需要传锁是因为在等待之前申请了锁,等待的时候需要释放锁,唤醒后需要重新申请锁
唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
——将等待的线程全部唤醒,即广播
int pthread_cond_signal(pthread_cond_t *cond);
——唤醒一个线程
2.2 实现原理
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构,其与普通的队列区别在于:
当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素
当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出
(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
上文中我们介绍了生产者消费者模型,简单来说,我们需要满足321原则
321原则:
3:3种关系
2:2种角色,生产者(1 or n),消费者(1 or n) ——线程或者进程
1:一个交易场所,内存空间
- 队列我们可以使用STL中自带的queue,值得注意的是,这个队列是生产者和消费者共有的,所以生产者和消费者在维护这个队列的时候,需要保持互斥性
- 当队列为空的时候无法消费,当队列为满的时候无法生产,这是拥塞的规则,我们需要用条件变量来约束
- 当生产者Push的时候可以唤醒消费者消费,当消费者Pop的时候可以唤醒生产者生产
2.3 实现代码
BlockQueue.hpp
#pragma once
#include <queue>
#include "LockGuard.hpp"
#define MaxSize 5
template <class T>
class BlockQueue
{
public:
BlockQueue(int sz = MaxSize)
: _capacity(sz)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_ccond, nullptr);
}
void Push(const T &data)
{
//pthread_mutex_lock(&_mutex);
LockGuard lock(&_mutex);
while (IsFull())
{
pthread_cond_wait(&_pcond, &_mutex);
}
_q.push(data);
pthread_cond_signal(&_ccond);
//pthread_mutex_unlock(&_mutex);
}
void Pop(T &data)
{
//pthread_mutex_lock(&_mutex);
LockGuard lock(&_mutex);
while (IsEmpty())
{
pthread_cond_wait(&_ccond, &_mutex);
}
data = _q.front();
_q.pop();
pthread_cond_signal(&_pcond);
//pthread_mutex_unlock(&_mutex);
}
bool IsEmpty()
{
if (_q.size() == 0)
{
return true;
}
else
{
return false;
}
}
bool IsFull()
{
if (_q.size() == _capacity)
{
return true;
}
else
{
return false;
}
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_ccond);
}
private:
std::queue<T> _q;
int _capacity;
pthread_mutex_t _mutex;
pthread_cond_t _pcond;
pthread_cond_t _ccond;
};
Main.cc
#include <iostream>
#include <pthread.h>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include "BlockQueue.hpp"
#include "Task.hpp"
using namespace std;
template <class T>
class ThreadData
{
public:
BlockQueue<T> *bq;
string name;
};
template <class T>
void *producer(void *argv)
{
ThreadData<T> *td = static_cast<ThreadData<T> *>(argv);
while (true)
{
int x_data = rand() % 10;
int y_data = rand() % 10;
char op_data = opers[rand() % opers.size()];
td->bq->Push(Task(x_data, y_data, op_data));
string qus;
qus += td->name + ": " + to_string(x_data) + " " + op_data + " " + to_string(y_data) + " = ?";
cout << qus << endl;
sleep(1);
}
}
template <class T>
void *consumer(void *argv)
{
ThreadData<T> *td = static_cast<ThreadData<T> *>(argv);
while (true)
{
Task data;
td->bq->Pop(data);
data();
cout<<td->name+ ": "+data.PrintResult()<<endl;
}
}
int main()
{
BlockQueue<Task> *bq = new BlockQueue<Task>;
ThreadData<Task> *p_td1 = new ThreadData<Task>;
ThreadData<Task> *p_td2 = new ThreadData<Task>;
ThreadData<Task> *p_td3 = new ThreadData<Task>;
ThreadData<Task> *c_td1 = new ThreadData<Task>;
ThreadData<Task> *c_td2 = new ThreadData<Task>;
p_td1->bq = bq;
p_td2->bq = bq;
p_td3->bq = bq;
c_td1->bq = bq;
c_td2->bq = bq;
p_td1->name = "p_th1";
p_td2->name = "p_th2";
p_td3->name = "p_th3";
c_td1->name = "c_th1";
c_td2->name = "c_th2";
pthread_t p[3], c[2];
srand((unsigned int)time(nullptr) ^ getpid());
pthread_create(&p[0], nullptr, producer<Task>, p_td1);
pthread_create(&p[1], nullptr, producer<Task>, p_td2);
pthread_create(&p[2], nullptr, producer<Task>, p_td3);
pthread_create(&c[0], nullptr, consumer<Task>, c_td1);
pthread_create(&c[1], nullptr, consumer<Task>, c_td2);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
return 0;
}
2.4 细节补充
关于条件变量等待的细节补充:
首先等待的接口需要传一把锁,因为在等待之前必然是申请到了锁才能访问临界资源,只不过因为条件不满足需要等待执行,执行等待之前需要把锁释放,不然会出现死锁现象,当条件满足的时候会进行唤醒,被唤醒的线程需要重新竞争锁,被唤醒的进程需要重新检查是否满足条件,因此上图中的判断是while而不是if,如果是if的话存在伪唤醒的情况,即唤醒的时候是满足条件的,但由于其他线程的操作再次不满足,但此时被唤醒的线程跳过了条件判断
三、基于环形队列的生产消费模型
3.1 POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX可以用于线程间同步
如何理解信号量:
- 信号量的本质是一把计数器
- 申请信号本质就是预订资源
- PV操作是原子的
如果把整个公共资源当作一个整体,将这个整体分为若干个小区域,分别分发给对应的线程,每个线程只能访问被分配到的区域内的资源
申请信号量的本质就是在申请一片小区域,一但申请成功,访问的位置就是互斥的,就不需要额外判断了,申请信号量就已经判断了资源是否就绪
信号量接口:
初始化信号量:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量:
int sem_destroy(sem_t *sem);
等待信号量:
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布信号量:
功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加1
int sem_post(sem_t *sem);//V()
3.2 实现原理
环形队列采用数组模拟,用模运算来模拟环状特性
要求:
1.生产者不能把消费者套一个圈
2.消费者不能超过生产者
生产者和消费者只有两种场景会指曲同一个位置:
a.为空——只能让生产者跑
b.为满——只能让消费者跑
其他情况,生产者和消费者根本就不会指向同一个位置!
对于生产者来说需要的资源是空间,对于消费者来说需要的是数据
每生产一个数据空间减少,数据增加
每消费一个数据空间增加,数据减少
3.3 实现代码
RingQueue.hpp
#pragma once
#include <vector>
#include <string>
#include <pthread.h>
#include <semaphore.h>
#include "LockGuard.hpp"
#include "Task.hpp"
#define DefaultSize 5
template <class T>
class RingQueue
{
private:
void P(sem_t *psem)
{
sem_wait(psem);
}
void V(sem_t *psem)
{
sem_post(psem);
}
public:
RingQueue(int capacity = DefaultSize)
: _v(capacity), _capacity(capacity), _p_step(0), _c_step(0)
{
sem_init(&_space_sem, 0, _capacity);
sem_init(&_data_sem, 0, 0);
pthread_mutex_init(&_p_mutex, nullptr);
pthread_mutex_init(&_c_mutex, nullptr);
}
void Push(const T &data)
{
P(&_space_sem);
{
LockGuard lock(&_p_mutex);
_v[_p_step] = data;
_p_step++;
_p_step %= _capacity;
}
V(&_data_sem);
}
void Pop(T &data)
{
P(&_data_sem);
{
LockGuard lock(&_c_mutex);
data = _v[_c_step];
_c_step++;
_c_step %= _capacity;
}
V(&_space_sem);
}
~RingQueue()
{
sem_destroy(&_space_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&_p_mutex);
pthread_mutex_destroy(&_c_mutex);
}
private:
vector<T> _v;
int _capacity;
int _p_step;
int _c_step;
sem_t _space_sem;
sem_t _data_sem;
pthread_mutex_t _p_mutex;
pthread_mutex_t _c_mutex;
};
Main.cc
#include <iostream>
#include "RingQueue.hpp"
#include <unistd.h>
using namespace std;
void *producer(void *arge)
{
RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(arge);
while (true)
{
int x_data = rand() % 10;
int y_data = rand() % 10;
char op_data = opers[rand() % opers.size()];
rq->Push(Task(x_data, y_data, op_data));
string qus;
qus += to_string(x_data) + " " + op_data + " " + to_string(y_data) + " = ?";
cout << qus << endl;
sleep(1);
}
return nullptr;
}
void *consumer(void *arge)
{
RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(arge);
while (true)
{
Task data;
rq->Pop(data);
data();
cout << data.PrintResult() << endl;
}
return nullptr;
}
int main()
{
RingQueue<Task> *rq = new RingQueue<Task>;
pthread_t p[3], c[2];
pthread_create(&p[0], nullptr, producer, rq);
pthread_create(&p[1], nullptr, producer, rq);
pthread_create(&p[2], nullptr, producer, rq);
pthread_create(&c[0], nullptr, consumer, rq);
pthread_create(&c[1], nullptr, consumer, rq);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
return 0;
}
3.4 细节补充
我们用两个整型分别来代表当前消费者和生产者所分配的位置
int _p_step;
int _c_step;
如果是单生产单消费的话,环形队列中并不需要申请锁,因为生产者和消费者只有一个,自然满足了生产者之间和消费者之间的互斥关系,而信号量的申请也满足了生产者和消费者之间互斥与同步之间的关系
但如果是多生产多消费的情况,信号量只能保证生产者和消费者之间互斥与同步之间的关系,并不能保证生产者之间和消费者之间的互斥关系,这时就需要两把锁分别来保证生产者之间和消费者之间的互斥关系,也就是分别维护 int _p_step;
int _c_step;
先申请信号量还是先申请锁?
先申请信号量
可以有效减少锁的竞争,并且只要申请到了锁就能进行操作,如果先申请锁没有信号量则对还需要等待信号量就绪