4 手写生产者—消费者模型(线程同步的起点)
1 引言
生产者—消费者模型算得上是初学者学习线程同步的一个好例子了。如果你之前看过线程池的构造话,那该模型的构造应该很简单。如果没看过的话,可以看完本篇博客之后再研究一下线程池🤗:
在线程池中,具有一个任务队列,用户(生产者)往其中添加线程,工作线程(消费者)往其中取出任务并执行,线程池就是一个典型的生产者—消费者模型。
2 模型介绍
顾名思义,该模型提供生产与消费两个功能,说简单些,就是有一块公共区域,一些人可以不断往里面放东西,另一些人可以不断往里面拿东西,但由于公共区域的大小是有限的,那么就要考虑一个问题,什么时候我们可以往里面放东西,什么时候我们又可以往里面拿东西了?这就是需要考虑线程同步的地方。
那么根据上面的描述,这个模型分为几个部分大致上是可以知晓了:
队列:相当于线程池的任务队列,有的也称为缓冲区。存放生产者生产的物品;
生产者函数:生产物品,并将其放置在任务队列中;
消费者函数:从队列中获取资源并消费;
概要图如下所示:

2.1 队列
队列的选择
首先我们要考虑如何消费队列的资源,是先到先消费,还是说按资源优先级来处理。在这里我们只考虑最简单的方式,先到先处理。
那么这样的要求意味着队列需要是一种能够便于从头部操作数据的结构,我们可以选择deque与queue,如果想队列具有更强的适配性,也可以使用模板。
queue<int> que;
2.2 线程安全
我们先不着急考虑生产者与消费者函数,在陈硕老师写的《Linux多线程服务端编程》中对线程安全的class有过以下定义(书写的很好,陈老师还是北师范的学生,真是榜样🤤):
1)多个线程同时访问时,能够表现出正确的行为;
2)不必关心操作系统如何调度这些线程,包括线程的执行顺序,线程间的交织;
3)调度方不必再做额外的同步或者协调操作;
能够做到这三点,就可以说这个class是线程安全的了,当然我们要求不会那么高(博主暂时也写不出这么perfect的代码🤣),在这里我们只要求做到简单的线程同步即可,这也是多线程模型最关注的点之一。
由于队列属于共享资源区,我们从上文的概述图可以看出,会出现多个线程同时对队列进行操作的情况,生产者们不断往队列中加东西,消费者们不断从队列中拿东西。那么,线程同步问题来了:生产者如何知道什么时候去生产?消费者又如何知道什么时候去消费了?
咋一眼看到这个问题,就会萌生一个很朴素的想法,你不知道,你去问不就完事了?生产者去访问队列看看还能不能往队列中放东西,消费者去访问队列看看还有东西可拿没。但是我们回过头来想,在多线程的环境下,倘若每个线程都这样无休止的访问队列,那CPU就尽在处理这些事情了,为此,在线程同步中有一种很经典的做法:mutex + condition_variable。
我们每次访问队列时,都先获取一把队列锁,然后使用同步变量的wait方法进行等待并释放该锁,被唤醒时争抢到队列锁再进行业务处理。当然,这种等待是有条件的,不然会存在虚假唤醒的情况。比如,对于生产者,我们只需要在队列非满的情况下唤醒等待中的生产者线程。
2.3 框架搭建
这里我们将这个类取名为Model,我们之前提到,这个类中存在三个基本元素:生产者、消费者与队列。其中由于队列为一块有限的共享区域,为了确保线程同步,我们需要一把队列锁与两个同步变量,一个同步变量用来同步生产者线程,一个用来同步消费者线程。
有时候,我们往往需要手动终止模型的运转,这时我们需要一个flag值,来通知所有线程立即结束工作,我们可以将其放置在生产者和消费者的wait判断条件中,若该flag为true,则等待的线程被唤醒,然后做一次条件判断,为真,立马终止函数执行。类似于下面的操作:
m_cv.wait(lock, [this]() { return m_stop || m_que.size() < m_capacity(); });
if (m_stop) {
cerr << "xxx is stopped" << endl;
return;
}
那么其实大概得框架已经出来了,框架如下:
class Model {
public:
Model(int _cap = 10); // 默认容量大小为10
void Prodecer(int iterm);
void Consumer();
void SetStop(); // 用于手动终止模型
private:
int m_capacity; // 缓冲区所能承受的最大容量
queue<int> m_que;
mutex que_mtx;
condition_variable producer_cv;
condition_variable consumer_cv;
bool m_stop;
};
2.4 生产者与消费者
生产者与消费者的逻辑非常简单,就如上文所说,访问队列时获取锁,然后释放锁进行条件等待,条件满足后争抢锁,然后进行业务处理,需要注意的两个点是:
1)生产者或消费者完成处理时需要互相通知:当生产者完成生产时,应该通知消费者(唤醒消费者),因为,消费者可能由于队列中无资源消费而处于等待状态,需要手动通知;同样,消费者完成消费时,应该通知生产者(唤醒生产者);
2)生产者与消费者被唤醒的条件:生产者的等待逻辑是:当队列满(队列的大小大于我们规定的容量)或者停止指标为false的时候处于等待态,等待逻辑反过来就是被唤醒的条件;消费者的等待逻辑是:当队列空或者停止指标为false的时候处于等待态;
多说无益,Talk is cheap,show me your code😁。
Producer:
void Model::Producer(int iterm) {
unique_lock<mutex> lock(que_mtx);
producer_cv.wait(lock, [this]() { return m_stop || m_que.size() < m_capacity; });
if (m_stop) { // 做一次条件判断
cerr << "Producer is stopped" << endl;
return;
}
m_que.push(iterm);
cout << "put iterm" << endl;
consumer_cv.notify_one();
// 假设通知完消费者后,还有其他逻辑,那么应该尽早释放锁
// lock.unlock();
}
Consumer:
void Model::Consumer() {
unique_lock<mutex> lock(que_mtx);
consumer_cv.wait(lock, [this]() { return !m_que.empty() || m_stop; });
if (m_stop) {
cerr << "Consumer is stopped" << endl;
return;
}
m_que.pop();
cout << "pop front" << endl;
producer_cv.notify_one();
// 假设通知完生产者后,还有其他逻辑,那么应该尽早释放锁
// lock.unlock();
}
2.5 终止函数
终止函数的处理逻辑就是将停止变量置为true,然后通知所有生产者与消费者线程,生产者与消费者线程检查停止变量值,自然终止执行。当然,m_stop也是一个共享变量,修改它时,也是需要获取锁再操作的。
void Model::SetStop() {
lock_guard<mutex> lock(que_mtx);
m_stop = true;
producer_cv.notify_all();
consumer_cv.notify_all();
}
2.6 生产者—消费者模型代码
总的代码如下,测试也包含在其中了:
点击查看代码
#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
using namespace std;
class Model {
public:
Model(int _cap = 10);
void Producer(int iterm);
void Consumer();
void SetStop();
private:
int m_capacity; // 缓冲区所能承受的最大容量
queue<int> m_que;
mutex que_mtx;
condition_variable producer_cv;
condition_variable consumer_cv;
bool m_stop;
};
inline Model::Model(int _cap) : m_capacity(_cap), m_stop(false) {
if (m_capacity <= 0) {
cerr << "arg Error" << endl;
exit(1);
}
}
void Model::SetStop() {
lock_guard<mutex> lock(que_mtx);
m_stop = true;
producer_cv.notify_all();
consumer_cv.notify_all();
}
void Model::Producer(int iterm) {
unique_lock<mutex> lock(que_mtx);
producer_cv.wait(lock, [this]() { return m_stop || m_que.size() < m_capacity; });
if (m_stop) {
cerr << "Producer is stopped" << endl;
return;
}
m_que.push(iterm);
cout << "put iterm" << endl;
consumer_cv.notify_one();
// 假设通知完消费者后,还有其他逻辑,那么应该尽早释放锁
// lock.unlock();
}
void Model::Consumer() {
unique_lock<mutex> lock(que_mtx);
consumer_cv.wait(lock, [this]() { return !m_que.empty() || m_stop; });
if (m_stop) {
cerr << "Consumer is stopped" << endl;
return;
}
m_que.pop();
cout << "pop front" << endl;
producer_cv.notify_one();
// 假设通知完生产者后,还有其他逻辑,那么应该尽早释放锁
// lock.unlock();
}
int main() {
vector<thread> threads;
Model mol(5);
// 创建生产者线程
for (int i = 0; i < 3; ++i) {
threads.emplace_back(&Model::Producer, &mol, i);
}
// 创建消费者线程
for (int i = 0; i < 3; ++i) {
threads.emplace_back(&Model::Consumer, &mol);
}
// 停止生产者和消费者,如果中途开启可能会导致生产者与消费者都没有执行完终止
// mol.SetStop();
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
3 参考文献
[1]《Linux多线程服务端编程》陈硕

浙公网安备 33010602011771号