优雅实现经典的生产者消费者模式
这是 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 需要付出更多的时间、性能成本。
来源网络
推荐阅读
认真的人,自带光芒!