服务器开发系列之
前言
生产者消费者模型本身主要是利用一个缓冲区来存储数据,从而减弱生产者和消费者之间的直接关系,提高多线程下程序效率。
一、生产者消费者实现流程
环境准备
实现语言:C++.
C++11提供的thread库
互斥锁mutex
条件变量condition_variable
队列queue
原子操作atomic
开发环境是Centos7.5。
实现细节
前提:
构建一个queue来存储生产的数据,queue不满时可以生产,不空时可以消费。
这个队列,采用阻塞队列的实现思路。
a)先实现构造函数,初始化一个unique_lock供condition_variable使用。
对于条件变量,申请两个,分别控制consumer和producer间的同步。
b)入和出队列的细节。
首先加锁。
循环判断一下目前的队列情况,对于各自的特殊情况(队满和队空)进行处理。
唤醒一个线程来处理特殊情况。
等待处理完毕。
处理入和出队列操作。
c)最后释放锁。
生产者与消费者任务的常见问题
1、对于多线程很多细节不是很理解
2、如何判断多线程同步是成功的。如何调试bug。
3、BUG:在多个consumer线程的情况下,会出现有些线程无法退出的情况(因为在wait等待操作)。
在析构函数里
加入stop,并且唤醒因条件变量阻塞的线程。
在pop函数中,
加入对stop的判断,当队列为空并且stop时,退出pop函数。
对consumer的条件变量wait调用加入pred,队列为空或者没有停止时阻塞。
4、之前对条件变量的wait函数理解不够深入,这次学习了一下。
单参数版本,此时传入一个unique_lock类型的变量,并且已经加锁。调用wait之后释放锁,并阻塞等待notify唤醒。唤醒后加锁。要注意的是被唤醒后有可能加锁失败,此时继续阻塞。
双参数版本,此时需要再加入一个Predicate类型的变量,应该是一个返回bool的函数。我一般用lamda表达式代替。返回false阻塞,true解除。要注意这里的意思是即使notify了,如果后面的条件不满足,也不会解除阻塞。
5、对于多consumer的消息同步暂时没做,是在外部程序完成调用的stop。
二、源码实现
BlockQueue.h
#pragma once
#include <mutex>
#include <thread>
#include <queue>
#include <atomic>
#include <memory>
#include <condition_variable>
#include <iostream>
int test();
template <class T>
class BlockQueue
{
public:
BlockQueue();
~BlockQueue();
void stop(){
_stopped.store(true);
_cv_con.notify_all();
}
bool available(){
return !stopped() || !empty();
}
//when full with Unlimited wait without drop data
void push(const T &data);
void pop(T &data);
//when full with with drop data
void push_full_drop(const T &data);
private:
bool stopped() {
return _stopped.load();
}
bool empty() {
return (int)_datas.size() == 0 ? true : false;
}
bool full(){
return (int)_datas.size() == _capacity ? true : false;
}
private:
std::mutex _mt;
std::condition_variable _cv_con;
std::condition_variable _cv_prod;
std::queue<T> _datas;
const int _capacity;
std::atomic<bool> _stopped;
std::atomic<uint64_t> _dropdataNUm;
};
BlockQueue.cpp
#include "producer_consume.h"
#define CAPACITY 50
template <class T>
BlockQueue<T>::BlockQueue() :_capacity(CAPACITY), _stopped(false),_dropdataNUm(0)
{
}
template <class T>
BlockQueue<T>::~BlockQueue()
{
stop();
_cv_con.notify_all();
_cv_prod.notify_all();
}
template <class T>
void BlockQueue<T>::push(const T &data)
{
std::unique_lock<std::mutex> _lck(_mt);
while (full()) {
_cv_con.notify_one();
_cv_prod.wait(_lck);
}
_datas.push(data);
_cv_con.notify_one();
}
template <class T>
void BlockQueue<T>::push_full_drop(const T &data)
{
std::unique_lock<std::mutex> _lck(_mt);
while(full()) {
_cv_con.notify_one();
if(_cv_prod.wait_for(_lck,std::chrono::milliseconds(1000)) == std::cv_status::timeout){
_dropdataNUm++;
if(_dropdataNUm >= UINT64_MAX){
_dropdataNUm = 0;
}
}
return;
}
_datas.push(data);
_cv_con.notify_one();
}
template <class T>
void BlockQueue<T>::pop(T &data)
{
std::unique_lock<std::mutex> _lck(_mt);
while (empty()){
if (this->stopped())
return;
_cv_prod.notify_one();
// cout << "Queue is empty, notify one producer...\n";
_cv_con.wait(_lck, [this]() { return this->stopped() || !this->empty(); }); //返回false阻塞,true解除
}
data = _datas.front();
_datas.pop();
_cv_prod.notify_one();
}
总结
本篇文章主要实现了一种满等不丢数据、满超时等后丢数据的生产者与消费者任务队列。可以根据自己的需求用于不同的场景。