多线程编程实战:基于阻塞队列与环形队列的生产者消费者模型详解

目录

一、理解生产者消费者模型

二、阻塞队列

框架设计

1.构造函数

2.Push函数实现

3.Pop函数实现

三、环形队列

框架设计

1.Push的实现

2.Pop的实现

四、源码

1.封装的锁,条件变量,信号量

1.1.Mutex.hpp

1.2.Cond.hpp

1.3.Sem.hpp

2.阻塞队列

2.1.BlockQueue.hpp

2.2.test.cpp

3.环形队列

3.1.RingQueue

3.2.test.cpp


一、理解生产者消费者模型

生产者和消费者所以指代的是:

  • 生产者:用于生成任务的线程。
  • 消费者:用与处理任务的线程。

        ⽣产者消费者模型是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。

它们之间有什么规律呢?举个例子:

  • 商品产商之间:当一个商家正在放把货物放到货台时另个商家不能同时去放要不然会乱,而只能等着。
  • 消费者之间:假设只有一个货物,当一个消费者正在取这个货物时 另一个消费者不能同时去取,只能等着。
  • 产商与消费者之间:(1).一个产商正在放货物时消费者不能同时去取。(2).当没有消费者想要的商品时,消费者就不要反复的来超市找了,回家等有了商品商家会通知你

商家:生产者,货物:数据(任务),交易场所:阻塞队列(或其它数据结构)。

可以得出生产者消费者模型的这些结论:

3种关系:

  • 生产者与生产者之间:互斥关系。
  • 消费者与消费者之间:互斥关系。
  • 生产者与消费者之间:互斥关系、同步关系。

2种角色:

  • 生产者角色和消费者角色(由线程承担)

1个交易场所:

  • 以特定结构构成的一块“内存”空间。

可以巧记为321原则。

生产者消费者模型高效的核心原因:

  • 消费者处理任务的过程和生产者生成任务的过程是并发的。这个两个过程是解耦的,可以实现忙闲不均

二、阻塞队列

        接下来我们就以阻塞队列为“交易场所”来实现一个生产者消费者模型。

框架设计

  • const size_t NUM = 5:用来做缺省参数,默认的队列大小。

成员函数数:

  • BlockQueue:构造函数。
  • Push:用于生产者把数据入队。
  • Pop:用于消费者获取数据。

成员变量

  • queue<T> qu:用来储存数据的一个队列。
  • Cond _pcond:条件变量,用来给生产者使用。
  • Cond _ccond:条件变量,用来给消费者使用。
  • Mutex _mutex:锁,所有线程两两之间是互斥的,需要所有线程共用一把锁。
  • int _pnum:生产者可以生产的数量。
  • int _cnum:消费者可以消费的数量。

注意:

        _pcond、_ccond、_pnum、_cnum都是用来维护同步机制的,因为生产者是否能生产和消费者是否能取到数据条件不一样,所以需要两个条件变量。

        Cond、Mutex和Sem等在上篇文章已经讲过封装,这里我就直接使用,大家可以去阅读以下文章:

Linux多线程编程的艺术:封装线程、锁、条件变量和信号量的工程实践-CSDN博客

当然不知道底层的封装也没关系,该篇文章一样能读懂的。

1.构造函数

        构造函数中只需要初始化后面两个int类型的成员就行,因为自定义类型在创建对象时就自动调用它们内部的构造函数了,同样在销毁时也会调它们的析构函数,这里也不用单独写析构。

        在刚开始队列为空,所以生产者可以生产的数量(_pnum)为sz,消费者可以消费的数量(_cnum)为0,如下:

BlockQueue(int sz = NUM)
    : _pnum(sz),
      _cnum(0)
{}

2.Push函数实现

当线程进入Push函数后,直接加锁,如果不满足条件就进行等待,否则就push。如下:

void Push(T data)
{
    _mutex.Lock();
    while (_pnum <= 0)
    {
        _pcond.Wait(_mutex);
    }
    qu.push(data);
    if (_cnum <= 0)
        _ccond.Signal();
    _pnum--, _cnum++;
    _mutex.Unlock();
}

        注意这里一定得是while循环,而不能用if判断,如果用if,那么多个生产者在等待时,当它们全部被唤醒后都将去竞争锁,当有一个得到锁然后push队列就满了,释放锁让其他生产者拿到再去进行push就出问题了,所以需要循环的去判断条件是否满足。

        在push完后队列绝对就不为空了,如果有消费者在等待需要进行唤醒。然后更新_pnum和_cnum。

        注:按正常逻辑走的话_pnum和_cnum是不可能小于0的,如果想要提高代码健壮性可以使用assert断言。

3.Pop函数实现

Pop是用来给消费者线程获取数据的,实现和Push类比如下:

T Pop()
{
    _mutex.Lock();
    while (_cnum <= 0)
    {
        _ccond.Wait(_mutex);
    }
    T ret = qu.front();
    qu.pop();
    if (_pnum <= 0)
        _pcond.Signal();
    _cnum--, _pnum++;
    _mutex.Unlock();
    return ret;
}

三、环形队列

        环形队列我们可以使用数组来模拟,它的空间是可以分为小块进行访问的,当生产者和消费者访问的地方不同它们是可以同时进行的,所以这里可以使用信号量来维护。

        环形队列,对空时end==head,队满时end==head。无法通过end和head是否相等来判断队满队空。
有常用的两种策略:

  1. 使用计数器。
  2. 添加一个空位,用end+1==head来表示队满,也就是把head-1的位置空出来。

这个模型相当于生产者和消费者在圆盘上做追逐游戏,我们做以下规则:

  1. 队空,生产者先行。
  2. 队满,消费者先行。
  3. 生产者不能把消费者套一个圈以上。
  4. 消费者不能超过生产者。

        按以上规则运行那么,只要不访问同一个位置,生产者消费者就可以并行。什么时候会访问同一个位置呢?

  • 队空:只能(互斥)生产者先(同步)运行。
  • 队满:只能(互斥)消费者先(同步)运行。

        所以我们用信号量来标记生产者可用的空间数,消费者可用的空间数,即P和V操作,这样就可以使执行流没有可用空间时是不会进入环形队列的,而进入环形队列它们就一定不是访问同一位置,所以生产者和消费者之间就可以相互独立,互不干扰。

框架设计

RingQueue(size_t sz):构造函数,需要传入一个数字,来确定这个环形队列需要开多大的空间。

  • Push:用于生产者把数据放入队列。
  • Pop:用于消费者从队列取得数据。

成员函数

  • _ringQueue:数组模拟环形队列。
  • _p_sem:生产者的信号量。
  • _p_index:生产者当前所处环形队列的下标。
  • _c_sem:消费者的信号量。
  • _c_index:消费者当前所处环形队列的下标。
  • _p_lock:生产者之间的锁。
  • _c_lock:消费者之间的锁。

因为生产者和消费者是否有可用的空间条件是不一样的,所以需要两个信号量。

        对于阻塞队列执行流之间两两互斥,所以只能用一个锁。而环形队列中,信号量把生产者和消费者之间完成解耦,所以生产者与消费者之间就不用再用锁来维护它们的互斥。

        锁的个数越多,执行流之间竞争越小,效率越高。

1.Push的实现

void Push(const T &data)
{
    _p_sem.P();
    LockGuard lock(_p_lock);
    _ringQueue[_p_index] = data;
    _p_index = (_p_index + 1) % _ringQueue.size();
    _c_sem.V();
}

注意:信号量P操作一定要放在申请锁前面。要理解这一点需要理解信号量的核心工作。如下:

        实际上在信号量内部是使用了条件变量和锁的,在队满和队空的时候信号量内是给线程加锁的,因为这时候生产者和消费者资源共享。那么信号量的优势到底在哪呢? 

        它的优势就是通过计数的方式准确的判断出哪些小块空间此时是否是共享 ,不是的话就“放过”,如果是就等待,所以在后面生产者和消费者之间就不需要再维护锁了。

2.Pop的实现

Pop是用来给消费者线程获取数据的,实现和Push类比,如下:

T Pop()
{
    _c_sem.P();
    LockGuard lock(_c_lock);
    T ret = _ringQueue[_c_index];
    _c_index = (_c_index + 1) % _ringQueue.size();
    _p_sem.V();
    return ret;
}

        相比阻塞队列,环形队列的效率显然更胜一筹,而阻塞队列是不能使用信号量的,因为它是queue类型只能把它看做一个整体,不能分成小块空间进行访问。 

四、源码

1.封装的锁,条件变量,信号量

1.1.Mutex.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
namespace my_mutex
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&_mutex);
        }
        pthread_mutex_t* GetLock()
        {
            return &_mutex;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex;
    };
    class LockGuard
    {
    public:
        LockGuard(Mutex& lock)
            : _lock(lock)
        {
            _lock.Lock();
        }
        ~LockGuard()
        {
            _lock.Unlock();
        }
    private:
        Mutex& _lock;
    };
}

1.2.Cond.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
using namespace my_mutex;
namespace my_cond
{
    class Cond
    {
    public:
        Cond()
        {
            pthread_cond_init(&_cond,nullptr);
        }
        void Wait(Mutex& _mutex)
        {
            pthread_cond_wait(&_cond,_mutex.GetLock());
        }
        void Signal()
        {
            pthread_cond_signal(&_cond);
        }
        void Broadcast()
        {
            pthread_cond_broadcast(&_cond);
        }
        ~Cond()
        {
            pthread_cond_signal(&_cond);
        }
    private:
        pthread_cond_t _cond;
    };
}

1.3.Sem.hpp

#pragma once
#include <semaphore.h>
#define DEF 1
class Sem
{
public:
    Sem(int val = DEF)
    {
        sem_init(&_sem,0,val);//??
    }
    void P()
    {
        sem_wait(&_sem);
    }
    void V()
    {
        sem_post(&_sem);
    }
    ~Sem()
    {
        sem_destroy(&_sem);
    }
private:
    sem_t _sem;
};

2.阻塞队列

2.1.BlockQueue.hpp

#include <iostream>
#include <pthread.h>
#include <queue>
#include "Cond.hpp"
using namespace std;
using namespace my_cond;
using namespace my_mutex;
const size_t NUM = 5;
namespace my_blockqueue
{
    template <typename T>
    class BlockQueue
    {
    public:
        BlockQueue(int sz = NUM)
            : _pnum(sz),
              _cnum(0)
        {}
        void Push(T data)
        {
            _mutex.Lock();
            while (_pnum <= 0)
            {
                _pcond.Wait(_mutex);
            }
            qu.push(data);
            if (_cnum <= 0)
                _ccond.Signal();
            _pnum--, _cnum++;
            _mutex.Unlock();
        }
        T Pop()
        {
            _mutex.Lock();
            while (_cnum <= 0)
            {
                _ccond.Wait(_mutex);
            }
            T ret = qu.front();
            qu.pop();
            if (_pnum <= 0)
                _pcond.Signal();
            _cnum--, _pnum++;
            _mutex.Unlock();
            return ret;
        }

    private:
        queue<T> qu;
        int _pnum; // 生产者可以生产的数量
        int _cnum;
        Mutex _mutex;
        Cond _pcond;
        Cond _ccond;
    };
}

2.2.test.cpp

#include "BlockQueue.hpp"
#include <string>
#include <unistd.h>
using namespace my_blockqueue;
BlockQueue<int> bq;
void* Run1(void* p)
{
    string name = static_cast<const char*>(p);
    int count = 1;
    while(true)
    {
        bq.Push(count++);
        cout<<name<<" send: "<<count<<endl;
    }
}
void* Run2(void* p)
{
    string name = static_cast<const char*>(p);
    while(true)
    {
        sleep(1);
        int count = bq.Pop();
        cout<<name<<" accept: "<<count<<endl;
    }
}
int main()
{
    pthread_t tid1[5],tid2[3];
    for(int i=0;i<5;i++)
        pthread_create(&tid1[i],nullptr,Run1,(void*)"tid1");
    for(int i=0;i<3;i++)
        pthread_create(&tid2[i],nullptr,Run2,(void*)"tid2");


    for(int i=0;i<5;i++)
        pthread_join(tid1[i],nullptr);
    for(int i=0;i<3;i++)
        pthread_join(tid2[i],nullptr);
    return 0;
}

3.环形队列

3.1.RingQueue

#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
using namespace std;
using namespace my_mutex;
namespace my_RingQueue
{
    template <typename T>
    class RingQueue
    {
    public:
        RingQueue(size_t sz)
            : _ringQueue(sz),
              _p_sem(sz),
              _p_index(0),
              _c_sem(0),
              _c_index(0)
        {
        }
        void Push(const T &data)
        {
            _p_sem.P();
            LockGuard lock(_p_lock);
            _ringQueue[_p_index] = data;
            _p_index = (_p_index + 1) % _ringQueue.size();
            _c_sem.V();
        }
        T Pop()
        {
            _c_sem.P();
            LockGuard lock(_c_lock);
            T ret = _ringQueue[_c_index];
            _c_index = (_c_index + 1) % _ringQueue.size();
            _p_sem.V();
            return ret;
        }

    private:
        vector<T> _ringQueue;
        Sem _p_sem;
        int _p_index;
        Sem _c_sem;
        int _c_index;
        Mutex _p_lock;
        Mutex _c_lock;
    };
}

3.2.test.cpp

#include <iostream>
#include "RingQueue.h"
#include <pthread.h>
#include <unistd.h>
using namespace std;
using namespace my_RingQueue;
RingQueue<int> rq(5);
void* fun1(void* p)
{
    string name = static_cast<char*>(p);
    int data = 1;
    while(true)
    {
        sleep(1);
        rq.Push(data++);
        cout<<name<<':'<<data-1<<endl;
    }
    return nullptr;
}
void* fun2(void* p)
{
    string name = static_cast<char*>(p);
    int val;
    while(true)
    {
        val = rq.Pop();
        cout<<name<<':'<<val<<endl;
    }
    return nullptr;
}
int main()
{

    pthread_t tid1[5],tid2[3];
    for(int i=0;i<5;i++)
    {
        char buffer[128];
        snprintf(buffer,sizeof(buffer),"p_thread-%d",i+1);
        pthread_create(&tid1[i],nullptr,fun1,(void*)buffer);
    }
    for(int i=0;i<3;i++)
    {
        char buffer[128];
        snprintf(buffer,sizeof(buffer),"p_thread-%d",i+1);
        pthread_create(&tid2[i],nullptr,fun2,(void*)buffer);
    }

    for(int i=0;i<5;i++)
        pthread_join(tid1[i],nullptr);
    for(int i=0;i<3;i++)
        pthread_join(tid2[i],nullptr);
    return 0;
}