查看原文
其他

优雅实现经典的生产者消费者模式

herongwei herongwei 2022-08-22

这是 herongwei 的第 89 篇原创

阅读本文大概需要 6 分钟。

生产者消费者问题是多线程并发中一个非常经典的问题,也是在互联网面试求职中会经常问到的一个题。


顾名思义,单生产者-单消费者模型中只有一个生产者和一个消费者,生产者不停地往队列库中放入产品,消费者则从队列库中取走产品。


有如下几个特点:

1、队列库容积有一定的限制,只能容纳一定数目的产品。

2、如果生产者生产产品的速度过快,则需要等待消费者取走产品之后,产品库不为空才能继续往产品库中放如新的产品。

3、如果消费者取走产品的速度过快,则可能面临产品库中没有产品可使用的情况,此时需要等待生产者放入一个产品后,消费者才能继续工作。


也就是:

1、当队列元素已满的时候,阻塞插入操作;

2、当队列元素为空的时候,阻塞获取操作;


01 

实现简单的生产者单消费者模型

(主要学习了 flag 哥的思路)

template<typename T>class ProAndCon {private: list<T> m_queue; mutex m_mutex;//全局互斥锁 condition_variable_any m_notEmpty;//全局条件变量(不为空) condition_variable_any m_notFull;//全局条件变量(不为满) int m_maxSize;//队列最大容量
private: //队列为空 bool isEmpty() const { return m_queue.empty(); } //队列已满 bool isFull() const { return m_queue.size() == m_maxSize; }
public: ProAndCon(int maxSize) { this->m_maxSize = maxSize; } ProAndCon();
virtual ~ProAndCon();
void product(const T& v) { lock_guard<mutex> locker(m_mutex); while(isFull()) { cout<<"队列已满,请等待"<<endl; //生产者等待"产品队列缓冲区不为满"这一条件发生. m_notFull.wait(m_mutex); } //往队列里面生产一个元素,同时通知不为空这个信号量 m_queue.push_back(v); m_notEmpty.notify_one(); } void consumption(T& v) { lock_guard<mutex> locker(m_mutex); while(isEmpty()) { cout<<"队列已空,请等待"<<endl; // 消费者等待"产品队列缓冲区不为空"这一条件发生. m_notEmpty.wait(m_mutex); } //在队列里面消费一个元素,同时通知队列不满这个信号量 v = m_queue.front(); m_queue.pop_front(); m_notFull.notify_one(); }};


02 

条件变量为什么要搭配互斥锁?


条件变量的改变一般是临界资源来完成的,那么修改临界资源首先应该加锁,而线程在条件不满足的情况下要阻塞,等待别人唤醒,那么在阻塞后一定要把锁放开,等到合适的线程拿到锁去修改临界资源,否则会出现死锁。


条件变量需要锁的保护;锁需要条件变量成立后,后重新上锁。


在线程被唤醒后第一件事也应该是争取拿到锁,恢复以前加锁的状态,否则在执行条件变量成立后的代码也法保证其原子性。


03

条件变量为什么要使用 while 循环判断,而不是 if?


首先我们应该了解,条件变量是用来阻塞或者唤醒线程的,阻塞和唤醒是依靠信号机制来处理的,而条件变量中的条件是我们自己规定的。


也就是说当条件成立后,操作系统才给阻塞进程发送信号,唤醒这个进程,那万一操作系统因为某些原因出故障了,随机给进程发送唤醒信号 (此时条件还没成立),线程醒了之后就会在条件没成立的情况下执行之后的代码。这也是第一点用 if 不能保证 bug 信号的有效处理。


04

std::unique_lock 与 std::lock_guard 的区别?

1、std::lock_guard 

std::lock_guard 是 RAII 模板类的简单实现,功能简单。

std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁,可通过上面代码查看。

在多线程编程中,使用较多,因此 C++11  提供了 lock_guard 模板类。在实际编程中,我们也可以根据自己的场景编写 resource_guard  RAII 类,避免忘掉释放资源。

来源网络

2、std::unique_lock

unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。

unique_lock 比 lock_guard 使用更加灵活,功能更加强大。

使用 unique_lock 需要付出更多的时间、性能成本。

来源网络


推荐阅读

经典书籍推荐

《STL 源码剖析》之关联式容器

《STL 源码剖析》内存管理,迭代器

秋招一堆神仙打架,我该如何应战?

认真的人,自带光芒!

原创不易

点个在看呗

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存