[Linux] Producer consumer model - blocking queue BlockQueue

1. Producer-consumer model

understanding of production and consumption

Introduction: For example, if our students want to buy things, but if there is no trading place supermarket, then we can only go to the supplier to buy things, then we can only generate one if we want one supplier, for For the suppliers, the generated cost is too high, so there is the existence of the medium of the trading place supermarket. The purpose is to concentrate demand and distribute products.

image-20230420084005988

Consumers and producers trade through supermarkets. When the producer does not need it, the supplier can still continue the sound field, and when the supplier no longer produces it, the consumer can still buy it! In this way, production and consumption can be decoupled. And we call the temporary location of Baocheng products the buffer zone.

The process of production and the process of consumption - decoupling

Temporary product storage place - buffer zone

When the main function calls the function func, the main function will produce data and pass it to the function func, the function func will temporarily save the data, and the function func will consume the data, which is actually the production and consumption model. But when we call the function func, the main function does nothing, and blocks there waiting for the function to return. We call the relationship between the main function and the calling function func a strong coupling.

The producer-consumer model uses a container to solve the problem of strong coupling between producers and consumers.

Production and consumption relationship

image-20230420085851537

Both production and consumption must see the "supermarket", so the "supermarket" is a shared resource. Since it is a shared resource that involves multi-threaded access, then this shared resource must be protected

三种关系: producer and producer (mutual exclusion), consumer and consumer (mutual exclusion), producer and consumer (mutual exclusion && synchronization), mutual exclusion ensures the security of shared resources, synchronization is to improve access efficiency

二种角色: producer thread, consumer thread

一个交易场所: a buffer of a specific structure

If you want to write a production and consumption model, the essence is to maintain the 321 principle

Mining features :

1. Decoupling production threads and consumption threads in the future

2. Support the problem of uneven busyness and idleness during a period of production and consumption (the buffer area has data and space)

3. Producers focus on production, consumers focus on consumption, and improve efficiency

If the supermarket buffer is full, the producer can only wait; if the supermarket buffer is empty, the consumer can only wait.


2. Production and consumption model based on blockqueue

Blocking queue: Blocking queue (Blocking Queue) is a data structure commonly used to implement the producer and consumer models

When the blocking queue is empty, the thread that gets the element from the blocking queue will be blocked until the blocking queue is put into the element.
When the blocking queue is full, the thread that puts elements into the blocking queue will be blocked until an element is taken out.

image-20230420093240803

Single production single consumption calculation

random number

The following takes single production and single consumption as an example:

//BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
const int gmaxcap =5;
template <class T>
class BlockQueue
{
public:
    BlockQueue(const int&maxcap = gmaxcap):_maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_pcond,nullptr);
        pthread_cond_init(&_ccond,nullptr);
    }
    void push(const T& in)
    {
        pthread_mutex_lock(&_mutex);
        while(is_full())
        {
            pthread_cond_wait(&_pcond,&_mutex);//因为生产条件不满足无法生产,此时我们的生产者进行等待
        }
        _q.push(in);
        //pthread_cond_signal:这个函数可以放在临界区内部,也可以放在外部
        pthread_cond_signal(&_ccond);
        pthread_mutex_unlock(&_mutex);
        //pthread_cond_signal(&_ccond);
    }
    void pop(T*out)//输出型参数,*,输入输出型:&
    {
        pthread_mutex_lock(&_mutex);
        while(is_empty())
        {
            pthread_cond_wait(&_ccond,&_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_pcond);
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }
private:
    bool is_empty(){return _q.empty();}
    bool is_full(){return _q.size()==_maxcap;}
private:
    std::queue<T> _q;
    int _maxcap;//队列上限
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond;//生产者条件变量
    pthread_cond_t _ccond;//消费者条件变量
};

//mainCp.cc
void* consumer(void * bq_)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(bq_);
    while(true)
    {
        int data;
        bq->pop(&data);
        std::cout<<"消费数据: "<<data<<std::endl;
        sleep(1);
    }
    return nullptr;
}
void*productor(void*bq_)
{
    BlockQueue<int>*bq = static_cast<BlockQueue<int>*>(bq_);
    while(true)
    {
        int data = rand()%10+1;
        bq->push(data);
        std::cout<<"生产数据: "<<data<<std::endl;
    }
    return nullptr;
}
int main()
{
    srand((unsigned long)time(nullptr)^getpid());
    BlockQueue<int> *bq = new  BlockQueue<int>();
    pthread_t c,p;
    pthread_create(&c,nullptr,consumer,bq);
    pthread_create(&p,nullptr,productor,bq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    return 0;
}

The second parameter of the pthread_cond_wait function must be the mutex we are using. It will wait when it is full. If the lock is taken away as before, other threads will not be able to access the shared resource. a.pthread_cond_wait: When this function is
called , will release the lock in an atomic way, and suspend itself
b.pthread_cond_wait: This function will automatically re-acquire the lock you passed in when it is woken up

Pthread_cond_signal false wakeup: Judgment problem: Suppose there are 10 producers and only one consumer, consume the data, if pthread_cond_broadcast wakes up 10 threads at the same time, but only needs to produce one data, and wakes up 10 threads at the same time If it is judged by if, there will be problems with push

image-20230420105755836

If the producer produces slowly, the consumer consumes quickly : produce one and consume one, and consume the latest data

image-20230420112933795

If the producer produces fast and the consumer consumes slowly : after stabilization, consume one and produce one

image-20230420113649157

Calculator task Task

Task.hpp: contains the callback function of func_t, which is the callback function for data calculation

#pragma once
#include <iostream>
#include <functional>
#include <cstdio>
class Task
{
    using func_t = std::function<int(int,int,char)>;
public:
    Task()
    {}
    Task(int x,int y,char op,func_t func)
    :_x(x),_y(y),_op(op),_callback(func)
    {}
    std::string operator()()
    {
        int result = _callback(_x,_y,_op);
        char buffer[1024];
        snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer,sizeof buffer,"%d %c %d = ?",_x,_op,_y);
        return buffer;
    }
private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

BlockQueue.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
const int gmaxcap =500;
template <class T>
class BlockQueue
{
public:
    BlockQueue(const int&maxcap = gmaxcap):_maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_pcond,nullptr);
        pthread_cond_init(&_ccond,nullptr);
    }

    void push(const T& in)
    {
        pthread_mutex_lock(&_mutex);
        while(is_full())
        {
            pthread_cond_wait(&_pcond,&_mutex);
        }
        _q.push(in);
        pthread_cond_signal(&_ccond);
        pthread_mutex_unlock(&_mutex);
    }

    void pop(T*out)
    {
        pthread_mutex_lock(&_mutex);
        if(is_empty())
        {
            pthread_cond_wait(&_ccond,&_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_pcond);
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }
private:
    bool is_empty(){return _q.empty();}
    bool is_full(){return _q.size()==_maxcap;}
private:
    std::queue<T> _q;
    int _maxcap;
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
};

Main.cc

#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include "BlockQueue.hpp"
#include "Task.hpp"
const std::string oper = "+-*/%"; 
int mymath(int x,int y,char op)
{
    int result = 0;
    switch (op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if (y == 0)
        {
            std::cerr << "div zero error!" << std::endl;
            result = -1;
        }
        else
            result = x / y;
    }
        break;
    case '%':
    {
        if (y == 0)
        {
            std::cerr << "mod zero error!" << std::endl;
            result = -1;
        }
        else
            result = x % y;
    }
        break;
    default:
        break;
    }
    return result;
}
void* consumer(void * bq_)
{
    BlockQueue<Task>* bq = static_cast<BlockQueue<Task>*>(bq_);
    while(true)
    {
        Task t;
        bq->pop(&t);
        std::cout<<"消费任务: "<<t()<<std::endl;
    }
    return nullptr;
}
void*productor(void*bq_)
{
    BlockQueue<Task>*bq = static_cast<BlockQueue<Task>*>(bq_);
    while(true)
    {
        int x = rand()%100+1;
        int y = rand()%10;
        int operCode = rand()%oper.size();
        Task t(x,y,oper[operCode],mymath);
        bq->push(t);
        std::cout<<"生产任务: "<<t.toTaskString()<<std::endl;  
        sleep(1);
    }
    return nullptr;
}
int main()
{
    srand((unsigned long)time(nullptr)^getpid());
    BlockQueue<Task> *bq = new  BlockQueue<Task>();
    pthread_t c,p;
    pthread_create(&c,nullptr,consumer,bq);
    pthread_create(&p,nullptr,productor,bq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    delete bq;
    return 0;
}

image-20230420121405826

storage task

Define the structure BlockQueues to encapsulate the blocking queue of computing tasks and the blocking queue of storage tasks, create producer threads, consumer threads, and storage threads to execute their respective methods

image-20230420201304498

blockqueue.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
const int gmaxcap = 500;
template <class T>
class BlockQueue
{   
public:
    BlockQueue(const int &maxcap = gmaxcap):_maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }
    void push(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        while(is_full())
        {
            pthread_cond_wait(&_pcond, &_mutex); 
        }
        _q.push(in);
        pthread_cond_signal(&_ccond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        while(is_empty())
        {
            pthread_cond_wait(&_ccond, &_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_pcond); 
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }
private:
    bool is_empty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _q.size() == _maxcap;
    }
private:
    std::queue<T> _q;
    int _maxcap;
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond; 
    pthread_cond_t _ccond;
};

Task.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <functional>

class CalTask
{
    using func_t = std::function<int(int,int,char)>;
    // typedef std::function<int(int,int)> func_t;
public:
    CalTask()
    {}
    CalTask(int x, int y, char op, func_t func)
    :_x(x), _y(y), _op(op), _callback(func)
    {}
    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
        return buffer;
    }
private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

const std::string oper = "+-*/%";

int mymath(int x, int y, char op)
{
    int result = 0;
    switch (op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if (y == 0)
        {
            std::cerr << "div zero error!" << std::endl;
            result = -1;
        }
        else
            result = x / y;
    }
        break;
    case '%':
    {
        if (y == 0)
        {
            std::cerr << "mod zero error!" << std::endl;
            result = -1;
        }
        else
            result = x % y;
    }
        break;
    default:
        // do nothing
        break;
    }

    return result;
}

class SaveTask
{
    typedef std::function<void(const std::string&)> func_t;
public:
    SaveTask()
    {}
    SaveTask(const std::string &message, func_t func)
    : _message(message), _func(func)
    {}
    void operator()()
    {
        _func(_message);
    }
private:
    std::string _message;
    func_t _func;
};

void Save(const std::string &message)
{
    const std::string target = "./log.txt";
    FILE *fp = fopen(target.c_str(), "a+");
    if(!fp)
    {
        std::cerr << "fopen error" << std::endl;
        return;
    }
    fputs(message.c_str(), fp);
    fputs("\n", fp);
    fclose(fp);
}

MianCp.cc

#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <ctime>
#include "blockqueue.hpp"
#include "Task.hpp"
template<class C,class K>
struct BlockQueues
{
    BlockQueue<C> *c_bq;
    BlockQueue<K> *s_bq;
};
void* productor(void*args)
{
    BlockQueue<CalTask>* bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(args))->c_bq;
    while(true)
    {
        int x = rand()%10+1;
        int y = rand()%5+1;
        int operCode = rand()%oper.size();
        CalTask t(x,y,oper[operCode],mymath);
        bq->push(t);
        std::cout<<"productor thread,生产计算任务: "<<t.toTaskString()<<std::endl;
        sleep(1);
    }
    return nullptr;
}
void* consumer(void*args)
{
    BlockQueue<CalTask>* bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(args))->c_bq;
    BlockQueue<SaveTask>* save_bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(args))->s_bq;
    while(true)
    {
        CalTask t;
        bq->pop(&t);
        std::string result = t();
        std::cout<<"cal thread,完成计算任务: "<<result<<" ... done"<<std::endl;

        SaveTask save(result,Save);
        save_bq->push(save);
        std::cout<<"cal thread,推送存储任务完成..."<<std::endl;
    }
    return nullptr;
}
void*saver(void*args)
{
    BlockQueue<SaveTask>* save_bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(args))->s_bq;
    while(true)
    {
        SaveTask t;
        save_bq->pop(&t);
        t();
        std::cout<<"save thread,保存任务完成..."<<std::endl;
    }
    return nullptr;
}
int main()
{
    srand((unsigned int)time(nullptr)^getpid());
    BlockQueues<CalTask,SaveTask> bqs;
    bqs.c_bq = new BlockQueue<CalTask>();
    bqs.s_bq = new BlockQueue<SaveTask>();
    pthread_t c,p,s;
    pthread_create(&p,nullptr,productor,&bqs);
    pthread_create(&c,nullptr,consumer,&bqs);
    pthread_create(&s,nullptr,saver,&bqs);
    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    pthread_join(s,nullptr);
    delete bqs.c_bq;
    delete bqs.s_bq;
    return 0;
}

image-20230420181958543

Produce more and consume more

You only need to modify MainCp.cc slightly to complete multiple production and multiple consumption , and the codes of other files do not need to be changed

MainCp.cc

#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include "blockqueue.hpp"
#include "Task.hpp"
template<class C,class S>
class BlockQueues
{
public:
    BlockQueue<C>*c_bq;
    BlockQueue<S>*s_bq;
}; 
void*productor(void*bqs_)
{
    BlockQueue<CalTask>* bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(bqs_))->c_bq;
    while(true)
    {
        int x = rand()%100+1;
        int y = rand()%10;
        int operCode = rand()%oper.size();
        CalTask t(x,y,oper[operCode],mymath);
        bq->push(t);
        std::cout<<"productor thread,生产计算任务: "<<t.toTaskString()<<std::endl;
        sleep(1);
    }
    return nullptr;
}
void* consumer(void * bqs_)
{
    BlockQueue<CalTask>* bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(bqs_))->c_bq;
    BlockQueue<SaveTask>* save_bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(bqs_))->s_bq;

    while(true)
    {
        CalTask t;
        bq->pop(&t);
        std::string result = t();
        std::cout<<"cal thread,完成计算任务: "<<result<<" ... done"<<std::endl;

        // SaveTask save(result,Save);
        // save_bq->push(save);
        // std::cout<<"cal thread,推送存储任务完成..."<<std::endl;
        // sleep(1);
    }
    return nullptr;
}
void* saver(void* bqs_)
{
    BlockQueue<SaveTask>* save_bq = (static_cast<BlockQueues<CalTask,SaveTask>*>(bqs_))->s_bq;
    while(true)
    {
        SaveTask t;
        save_bq->pop(&t);
        t();
        std::cout<<"save thread,保存任务完成..."<<std::endl;
    }
    return nullptr;
};
int main()
{
    srand((unsigned long)time(nullptr)^getpid());
    BlockQueues<CalTask,SaveTask> bqs;
    bqs.c_bq = new  BlockQueue<CalTask>();
    bqs.s_bq = new  BlockQueue<SaveTask>();
    pthread_t c[2],p[3];
    pthread_create(p,nullptr,productor,&bqs);
    pthread_create(p+1,nullptr,productor,&bqs);
    pthread_create(p+2,nullptr,productor,&bqs);
    pthread_create(c,nullptr,consumer,&bqs);
    pthread_create(c+1,nullptr,consumer,&bqs);
  
    pthread_join(c[0],nullptr);
    pthread_join(c[1],nullptr);
    pthread_join(p[0],nullptr);
    pthread_join(p[1],nullptr); 
    pthread_join(p[2],nullptr);

    delete bqs.c_bq;
    delete bqs.s_bq;
    return 0;
}

3. Summary

It is possible to have multiple producers and multiple consumers. This blocking queue must be locked before the process enters, and compete for this lock. Consumers and consumers also compete for locks

**In other words: In the blocking queue, no matter how many external threads there are, there is always only one thread that actually enters the blocking queue for production or consumption. **In a task queue, there are multiple producers and multiple consumers. Due to the existence of locks, only one execution flow is placed in the lock at any time.

生产消费模型高效在哪里:

For producers, it is necessary to place tasks into the blockQueue, and for consumers, it is necessary to take tasks from the blockQueue

For the producer, where does the task come from? Getting tasks and building tasks takes time

For the consumer, is it over after it takes the task out of the task queue? After the consumer gets the task, is there any follow-up task?

Efficiency is reflected in the fact that a task taken out by a thread may be doing calculations. While it is doing calculations, other threads can continue to take it from the queue and continue to do calculations, 高效并不是体现在从队列中拿数据高效! Instead, we can allow one or more threads to calculate multiple tasks concurrently! While calculating multiple tasks, it does not affect other threads and continues the process of taking tasks from the queue. In other words, 生产者消费者模型的高效:可以在生产之前与消费之后让线程并行执行don't think that the producer consumption mode is just the process of producing tasks to the queue, which is the production process. The production process: 1. It takes a little effort to get the task 2. Put it in the queue after getting it. The process of production; the entire process of consumption: it is not just to get the task into the context of the thread, but to calculate or store these tasks after getting it, which is the process of consumption. Before production and after consumption, we can have multiple threads Concurrent.

Guess you like

Origin blog.csdn.net/weixin_60478154/article/details/130274809