目录
1.生产者消费者模型
- 321原则(便于记忆)
生产者-消费者模型的321原则是指3个核心组件(生产者、消费者、共享缓冲区)、2种同步机制(互斥锁、条件变量)及1个临界区(保护共享缓冲区访问)的设计原则。
为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者模型优点
- 解耦
- 支持并发
- 支持忙闲不均
基于BlockingQueue的生产者消费者模型
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
2.C++ queue模拟阻塞队列的生产消费模型
为了便于大家理解,我们以双生产者,双消费者,来进行讲解。
BlockQueue.hpp代码部分
#pragma once #include <iostream> #include <string> #include <queue> #include <pthread.h> const static int defaultcap = 5; template <typename T> class BlockQueue { private: bool IsFull() { return _block_queue.size() == _max_cap; } bool IsEmpty() { return _block_queue.empty(); } public: BlockQueue(int cap = defaultcap) : _max_cap(cap) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_p_cond, nullptr); pthread_cond_init(&_c_cond, nullptr); } ~BlockQueue() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_p_cond); pthread_cond_destroy(&_c_cond); } void Pop(T *out) { pthread_mutex_lock(&_mutex); while (IsEmpty()) { pthread_cond_wait(&_c_cond, &_mutex); } // 1.不为空 || 2.被唤醒了 *out = _block_queue.front(); _block_queue.pop(); pthread_mutex_unlock(&_mutex); pthread_cond_signal(&_p_cond); } void Equeue(const T &in) { pthread_mutex_lock(&_mutex); while (IsFull()) { // 满了,生产者不能生产,必须等待 // 被调用的时候:除了让自己继续排队等待,还会自己释放传入的锁 // 函数返回的时候,不就还在临界区吗? // 返回时:必须先参与锁的竞争,重新上锁,该函数才会返回 pthread_cond_wait(&_p_cond, &_mutex); } // 1.要么没满 2.要么被唤醒了 _block_queue.push(in); // 生产到阻塞队列 pthread_mutex_unlock(&_mutex); pthread_cond_signal(&_c_cond); // 让消费者消费 } private: std::queue<T> _block_queue; // 临界资源 int _max_cap; pthread_mutex_t _mutex; pthread_cond_t _p_cond; // 生产者条件变量 pthread_cond_t _c_cond; // 消费者条件变量 };
我们使用阻塞式队列来充当我们的中转站,里面存放我们的任务,我们需要一个锁和两个条件变量来实现我们的互斥和同步。我们还可以设置一个队列存储的最大任务量,我们给它默认设置为5个。
然后,我们就可以设计这个模型了,我们先来初始化成员属性,初始化的步骤很简单,我们只需要将队列存放最大任务量的初始化值设置一下,以及将一锁两条件变量创建初始化好就可以了。析构函数的话自然就是把他们三个销毁就行了。
接下来就是该模型的核心部分了,我们既然是生产者消费者模型,那么我们自然是需要生产者生产任务,消费者获取任务的过程的。所以我们就需要两个成员函数,一个就是向队列派发任务,另一个就是向队列获取。
我们先来讲派发(Equeue):我们的思路很简单,首先,为了保证我们代码的安全性,我们需要加锁,我们需要判断队列是否已满,满了就等待消费者去消费,没满我们就可以生产,生产完后就可以通知消费者去消费(若消费者处于等待状态)。
消费者消费的逻辑(Pop)跟生产者派发过程是很像的:不同的地方就在于消费者判断等待条件是看队列是否为空,为空就等待,不为空就获取,队列就会少一个任务,我们就可以同样的去通知生产者生产。
我们看到生产者和消费者的参数类型不同,因为生产者的参数是为了传入,消费者是输出型参数,为了将数据带给上层,我们这么设计就是为了代码之间的解耦。
Task.hpp代码部分
#pragma once #include <iostream> #include <functional> using task_t = std::function<void()>; void Download() { std::cout << "我是一个下载的任务" << std::endl; }
我们就设计一个简答的打印任务就可以了,我们用函数包装器来包装它。
Main.cc代码
#include "BlockQueue.hpp" #include "Task.hpp" #include <pthread.h> #include <ctime> #include <unistd.h> void *Consumer(void *args) { BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args); while (true) { // 1.获取数据 task_t t; bq->Pop(&t); // 2.处理数据 t(); } } void *Productor(void *args) { srand(time(nullptr) ^ getpid()); BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args); while (true) { // 1.生产数据 bq->Equeue(Download); std::cout << "Productor -> Download" << std::endl; sleep(1); } } int main() { BlockQueue<task_t> *bq = new BlockQueue<task_t>(); pthread_t c, c1, p, p1; pthread_create(&c, nullptr, Consumer, bq); pthread_create(&p, nullptr, Productor, bq); pthread_create(&c1, nullptr, Consumer, bq); pthread_create(&p1, nullptr, Productor, bq); pthread_join(c, nullptr); pthread_join(p, nullptr); pthread_join(c1, nullptr); pthread_join(p1, nullptr); return 0; }
main.cc函数就是我们主逻辑的执行过程(也就是我们上面提到的上层)。我们让两个线程去生产,两个线程去消费。生产者我们随机让它等待一段时间去生产,由于我们的模型传进去是void*类型,我们需要将它强转(消费者同理),然后我们就可以一直派发任务了,消费者的任务就是获取,然后直接执行任务。
运行结果:
我们的打印会偶尔的混乱是因为我们没有在上层去进行很好的打印同步,这是正常现象,我们的执行逻辑是同步的。
07-13
1024
