C++ 11多线程编程(三)--生产者消费者队列及实现

生产者消费者队列

它是实现线程间协作,交互一种重要手段。从一端放数据,从另一端取数据。放入数据的线程称为生产者,取出数据的线程称为消费者。生产者和消费者可以有一个或多个。

  • 生产者,消费者线程间通过条件变量来实现协作

  • 对队列的访问需要加锁互斥

用途划分:

根据队列的用途来划分为两大类

  • 数据分发

队列中存放的业务数据。分别有一个或多个生产者,消费者线程。生产者线程产生不同类型的数据,通过队列分发给不同消费者线程

  • 任务队列

队列中存放的是可调用对象,分别有一个或多个生产者,消费者线程

  • 在调用要求时序的情况下,应该只有一个生产者和一个消费者线程,一个队列,时序的要求由队列的先进先出的特性保证
  • 在调用不要求时序的情况下,则可以有多个生产者,消费者线程

通过任务队列,来实现异步调用。发起业务操作的线程不会被阻塞,业务执行函数被放到另外一个线程执行,比如释放过程耗时高的资源且释放操作不能并发进行时。可以将释放操作专门放到一个线程中去做,此时可以有多个生产者线程,一个消费者线程,生产者将需要释放的资源对象放入队列,消费者线程依次取出后进行释放操作。此时就需要队列来实现异步操作

容量划分:

根据队列的容量划分为两大类

  • 有界队列

容量有大小限制,当满了后,生产者线程需要等待,为空时,消费者线程需要等待。这种队列可以用于数据分发的场景,但不能用于异步调用(异步调用的特性是生产者调用能马上返回,所以如果生产者阻塞在等待容器空闲显然是不满足要求的)

  • 无界队列

容量大小无限制,生产者线程可以一直放数据,队列为空的时候,消费者线程需要等待。这种队列即可以用于数据分发,也可以用于异步操作。这种情况下,消费者线程数量要大于生产者线程,是让数据或是任务及时被处理,避免堆积。

  • 在用途这个方向的划分,对于队列的实现,没有太大区别。只是队列中放的数据类型不一样。一种是业务的数据,一种是可调用的对象。

  • 那么有界队列和无界队列,在队列的实现方式上是有很大的不同。

实现

有界队列
  • 容量有限,对生产者线程来说,是需要等待队列中有空位,如果没有则应该等待。对消费者来说,是需要队列中有数据,如果没有也应该等待

  • 那么生产者放入数据后,应该通知消费者。消费者取走数据后应该通知生产者有空位

#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <iostream>

template<typename T>
class CBoundedQueue
{
public:
    CBoundedQueue(int maxSize):m_iMaxSize(maxSize),m_iCnt(0)
    {

    }

    ~CBoundedQueue()
    {

    }

    void Put(const T& x)
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        //有一个判断谓词,等价于while(!Pred){m_notFull.wait()}
        m_notFull.wait(lock, std::bind(&CBoundedQueue::CanPut,this));
        m_deqDatas.push_back(x);
        ++m_iCnt;
        if (m_iCnt > m_iMaxSize)
        {
            m_iCnt = m_iMaxSize;
        }
        
        //手动释放锁,减小锁的范围
        lock.unlock();

        //m_notEmpty.notify_one();
        m_notEmpty.notify_all();
    }

    T Get()
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        //有一个判断谓词,等价于while(!Pred){m_notEmpty.wait()}
        m_notEmpty.wait(lock, std::bind(&CBoundedQueue::CanGet,this));
        T front(m_deqDatas.front());
        m_deqDatas.pop_front();
        --m_iCnt;
        //手动释放锁,减小锁的范围
        lock.unlock();

        //m_notFull.notify_one();
        m_notFull.notify_all();
        return front;
    }

    bool Empty() const
    {
        std::lock_guard<std::mutex> lock;
        return m_deqDatas.empty();
    }

    bool Full() const
    {
        std::lock_guard<std::mutex> lock;
        return m_iCnt == m_iMaxSize;
    }

private:
    //判断谓词
    bool CanPut() { return m_iCnt < m_iMaxSize; }
    bool CanGet() { return m_iCnt > 0;}
private:
    //计数
    int m_iCnt;
    int m_iMaxSize;
    //容器类型为deque
    std::deque<T> m_deqDatas;
    //互斥量,对队列进行同步保护
    std::mutex m_mutex;
    //用于限制生产者线程
    std::condition_variable m_notFull;
    //用于限制消费者线程
    std::condition_variable m_notEmpty;
};

main函数

void consumer(CBoundedQueue<int>* dep)
{
    auto thread_id = std::this_thread::get_id();
    std::this_thread::sleep_for(std::chrono::seconds(3));
    while (1)
    {
        std::cout << "thread id "<<thread_id<<","<<dep->Get() << std::endl;
    }
}

int main()
{
    CBoundedQueue<int> dep(10);
    std::future<void> ret1 = std::async(std::launch::async, consumer, &dep);
    再产生一个消费者线程
    //std::future<void> ret2 = std::async(std::launch::async, consumer, &dep);

    
    for (int i = 0; i < 12; ++i)
    {
        dep.Put(i);
        std::cout << "put data " << i << std::endl;
    }

    ret1.wait();
    
    //ret2.wait();
}
  • 有界队列,必须在容器的容量上同时限制生产者和消费者线程,所以有两个信号量m_notFull,m_notEmpty
  • m_mutex 互斥量即是用于线程间同步的,包括生产者之间,消费者之间,生产者与消费者之间,也是保护条件变量。
  • 当队列满时,生产者线程阻塞在m_notFull上,消费者线程阻塞在m_notEmpty上。
  • m_notFull的语义是当消费者线程取出数据时,唤醒生产者线程有空位去放数据。
  • m_notEmpty的语义是当生产者线程放入一个数据时,唤醒消费者线程取出数据。
无界队列
#include <thread>
#include <condition_variable>
#include <mutex>
#include <deque>
#include <iostream>
#include <future>
template<typename T>
class UnBoundedQueue
{
public:
    UnBoundedQueue()
    {
    }

    void Put(const T&x)
    {
        //这个lock用于对生产者间的同步

        {
            std::lock_guard<std::mutex> lock(m_mutex);
            m_deqDatas.push_back(x);
        }


        //信号量用于生产者,消费者间的同步
        //m_notEmpty.notify_one();
        m_notEmpty.notify_all();
    }

    T Get()
    {
        //这个lock用于配合信号量的使用,
        std::unique_lock<std::mutex> lock(m_mutex);
        /*当后面的lambda返回false时,则信号量进入wait,此时自动释放lock
        *等待到信号量后,则再获取lock
        */
        m_notEmpty.wait(lock, [this] {return !m_deqDatas.empty(); });
        T front(m_deqDatas.front());
        m_deqDatas.pop_front();

        return front;
    }

    bool Empty()
    {
        //这个lock用于对消费者,生产者线程的互斥
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_deqDatas.empty();
    }

    size_t Size()
    {
        //这个lock用于对消费者,生产者线程的互斥
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_deqDatas.size();
    }


private:
    std::deque<T> m_deqDatas;
    //互斥量,对队列进行同步保护
    std::mutex m_mutex;
    //用于限制消费者线程
    std::condition_variable m_notEmpty;

};

main函数

void consumer(UnBoundedQueue<int>* dep)
{
    auto thread_id = std::this_thread::get_id();
    std::this_thread::sleep_for(std::chrono::seconds(3));
    while (1)
    {
        std::cout << "thread id " << thread_id << "," << dep->Get() << std::endl;
    }
}

int main()
{
    UnBoundedQueue<int> queue;
    auto f = std::async(std::launch::async, consumer, &queue);

    for (int i = 0; i < 100; ++i)
    {
        queue.Put(i);
    }

    f.wait();
}
  • 无界队列,生产者线程并不需要等待队列中有空位,所以相比有界队列不需要等待空位的条件变量。

  • 信号量m_notEmpty是用于在容器的容量上限制消费者线程,当容量为空时,消费者线程应该被阻塞。

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mo4776

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值