muduo源码阅读(5):BlockingQueue, BoundedBlockingQueue和ThreadPool类

1.了解生产者-消费者问题

生产者-消费者问题也被称为有界缓冲区问题,两个进程/线程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区中取出信息。

问题在于当缓冲区已满,而此时生产者还想向其中放入一个新的数据项的情况。其解决方法就是让生产者休眠,待消费者从缓冲区中取出一个或多个数据项时再唤醒它。同样地,当消费者试图从缓冲区中取数据而发现缓冲区为空时,消费者就休眠,直到生产者向其中放入一些数据时再将其唤醒。

2.无界缓冲BlockingQueue.h

#ifndef MUDUO_BASE_BLOCKINGQUEUE_H
#define MUDUO_BASE_BLOCKINGQUEUE_H

#include <muduo/base/Condition.h>
#include <muduo/base/Mutex.h>

#include <boost/noncopyable.hpp>
#include <deque>
#include <assert.h>

namespace muduo
{
    template<typename T>
    class BlockingQueue : boost::noncopyable
    {
    public:
        BlockingQueue()
            : mutex_(),
            notEmpty_(mutex_),
            queue_()
        {
        }
        // put函数模拟了生产一个产品的过程,首先加锁,然后在队列里加入产品后通知其他线程消费。
        void put(const T& x)                                                              //往队列放一个元素
        {
            MutexLockGuard lock(mutex_);
            queue_.push_back(x);
            notEmpty_.notify(); // wait morphing saves us                   /通知想取走元素又无元素可取的线程
          
        }

        T take()                                                         //取走一个元素
        {
            MutexLockGuard lock(mutex_);
            // always use a while-loop, due to spurious wakeup(虚假唤醒)
            while (queue_.empty())                                         //如果队列为空,阻塞等待
            {
                notEmpty_.wait();
            }
            assert(!queue_.empty());
            T front(queue_.front());
            queue_.pop_front();
            return front;
        }

        size_t size() const
        {
            MutexLockGuard lock(mutex_);
            return queue_.size();
        }

    private:
        /*
        私有成员:
            mutex_是前面封装过的互斥量。条件量notEmpty有两个用途,当消费者进行消费时,
            若没有产品了,notEmpty_可以用于阻塞;当生产者生产一个产品后,可以唤醒其他线程来消费。
            queue_是一个双向队列,作用是存储产品,生产者生产的产品数目不受限。
        
        */
        mutable MutexLock mutex_;
        Condition         notEmpty_;
        std::deque<T>     queue_;
    };

}

#endif  // MUDUO_BASE_BLOCKINGQUEUE_H

要点:take()函数

take函数模拟的是消费者消费的过程,加锁后首先检测队列中是否有产品,没有则等待,当有产品可以消费时取出。值得注意的是take函数里的while循环,有时候不留神会把它写成if,出现虚假唤醒的错误。所谓虚假唤醒,常见有两个原因,一种是条件变量的等待被信号中断。pthread 的条件变量等待 pthread_cond_wait 是使用阻塞的系统调用实现的(比如 Linux 上的 futex),这些阻塞的系统调用在进程被信号中断后,通常会中止阻塞、直接返回 EINTR 错误。而因为本线程拿到 EINTR 错误和重新调用 futex 等待之间,可能别的线程进行了操作,出现问题。第二种可能的原因是多核cpu中,pthread_cond_signal会唤醒多个等待的线程,由于线程调度的原因,被条件变量唤醒的线程在本线程内真正执行「加锁并返回」前,另一个线程插了进来,完整地进行了一套「拿锁、改条件、还锁」的操作。知乎:为什么条件锁会产生虚假唤醒现象(spurious wakeup)?

3.有界缓冲BoundedBlockingQueue.h

BoundedBlockingQueue.h与BlockingQueue.h大同小异,不同在于BoundedBlockingQueue的生产者队列有上限。


#ifndef MUDUO_BASE_BOUNDEDBLOCKINGQUEUE_H
#define MUDUO_BASE_BOUNDEDBLOCKINGQUEUE_H

#include <muduo/base/Condition.h>
#include <muduo/base/Mutex.h>

#include <boost/circular_buffer.hpp>
#include <boost/noncopyable.hpp>
#include <assert.h>

namespace muduo
{
    /*
        有界缓冲
    */
    template<typename T>
    class BoundedBlockingQueue : boost::noncopyable
    {
    public:
        explicit BoundedBlockingQueue(int maxSize)
            : mutex_(),
            notEmpty_(mutex_),
            notFull_(mutex_),
            queue_(maxSize)//队列元素数目限制
        {
        }

        void put(const T& x)       //放入一个元素
        {
            MutexLockGuard lock(mutex_);
            while (queue_.full())             //如果满了就阻塞等待
            {
                notFull_.wait();
            }
            assert(!queue_.full());
            queue_.push_back(x);
            notEmpty_.notify();            //通知可取了
        }

        T take()                         //取走一个元素
        {
            MutexLockGuard lock(mutex_);
            while (queue_.empty())             //如果空的就阻塞等待
            {
                notEmpty_.wait();
            }
            assert(!queue_.empty());
            T front(queue_.front());
            queue_.pop_front();
            notFull_.notify();                   //通知可放!
            return front;
        }

        bool empty() const
        {
            MutexLockGuard lock(mutex_);
            return queue_.empty();
        }

        bool full() const
        {
            MutexLockGuard lock(mutex_);
            return queue_.full();
        }

        size_t size() const
        {
            MutexLockGuard lock(mutex_);
            return queue_.size();
        }

        size_t capacity() const
        {
            MutexLockGuard lock(mutex_);
            return queue_.capacity();
        }

    private:
        mutable MutexLock          mutex_;
        Condition                  notEmpty_;
        Condition                  notFull_;
        boost::circular_buffer<T>  queue_;                                            //环形缓冲区!!
    };

}

#endif  // MUDUO_BASE_BOUNDEDBLOCKINGQUEUE_H

要点

mutex_是前面封装的锁。notEmpty有两个作用,一是在消费者消费时检查是否有产品可以消费,没有则等待;而是在生产者生产之后唤醒其他线程来消费;notFull有两个作用,一是生产者在生产者生产时判定队列是否已满;而是消费者消费完之后唤醒生产者来生产;queue_是一个boost::circular_buffer:circular_buffer是用一块连续内存保存数据,元素的下标从0到n - 1依次增大(begin处为0, end - 1处为n - 1)。如果达到容量上限,继续push_back方法压入元素时,原来begin处的元素就会被覆盖,原来begin + 1处的元素成为新的begin.

4.线程池ThreadPool

线程池问题本质上也是一个生产者-消费者问题
外部线程可以向线程池中的任务队列添加任务,相当于“生产者”;一旦任务队列中有任务,就唤醒线程队列中的线程来执行这些任务,这些线程就相当于“消费者”。模型如下图。

在这里插入图片描述
默认是无界缓冲,当然也可以通过设置ThreadPool构造函数中的maxQueueSize_值来设置有界缓冲区数目。
ThreadPool.h

#ifndef MUDUO_BASE_THREADPOOL_H
#define MUDUO_BASE_THREADPOOL_H

#include <muduo/base/Condition.h>
#include <muduo/base/Mutex.h>
#include <muduo/base/Thread.h>
#include <muduo/base/Types.h>

#include <boost/function.hpp>
#include <boost/noncopyable.hpp>
#include <boost/ptr_container/ptr_vector.hpp>

#include <deque>

namespace muduo
{

	class ThreadPool : boost::noncopyable
	{
	public:
		//任务函数
		typedef boost::function<void()> Task;
		explicit ThreadPool(const string& name = string("ThreadPool"));
		~ThreadPool();

		// Must be called before start().
		void setMaxQueueSize(int maxSize) { maxQueueSize_ = maxSize; }
		//设置线程初始回调函数(执行函数)
		void setThreadInitCallback(const Task& cb)
		{
			threadInitCallback_ = cb;
		}

		void start(int numThreads);
		void stop();
		// Could block if maxQueueSize > 0
		void run(const Task& f);


	private:
		bool isFull() const;
		void runInThread();
		Task take();

		MutexLock mutex_;
		Condition notEmpty_;
		Condition notFull_;
		string name_;
		Task threadInitCallback_;
		//boost::ptr_vector专门用于动态分配的对象,它使用起来更容易也更高效。
		boost::ptr_vector<muduo::Thread> threads_;			//线程队列(消费者)
		std::deque<Task> queue_;							//任务队列(生产者)
		size_t maxQueueSize_;
		bool running_;
	};

}

#endif

ThreadPool.cc

// Use of this source code is governed by a BSD-style license
// that can be found in the License file.
//
// Author: Shuo Chen (chenshuo at chenshuo dot com)

#include <muduo/base/ThreadPool.h>
#include <muduo/base/Exception.h>
#include <boost/bind.hpp>
#include <assert.h>
#include <stdio.h>

using namespace muduo;

ThreadPool::ThreadPool(const string& name)
    : mutex_(),
    notEmpty_(mutex_),
    notFull_(mutex_),
    name_(name),
    maxQueueSize_(0),       //0代表无界队列(default)
    running_(false)
{
}

ThreadPool::~ThreadPool()
{
    if (running_)
    {
        stop();
    }
}
// 启动线程池,启动的线程是固定个数的(numThreads)
void ThreadPool::start(int numThreads)              
{
    assert(threads_.empty());// 断言线程池是空的
    running_ = true;// 运行状态标记置为true
    threads_.reserve(numThreads); // 为线程池预留指定大小的空间
    // 创建线程
    for (int i = 0; i < numThreads; ++i)
    {
        char id[32];
        snprintf(id, sizeof id, "%d", i + 1);
        threads_.push_back(new muduo::Thread(
            boost::bind(&ThreadPool::runInThread, this), name_ + id));
        threads_[i].start();
    }
    if (numThreads == 0 && threadInitCallback_)         //线程池为空情况        
    {
        threadInitCallback_();
    }
}
// 关闭线程池
void ThreadPool::stop()
{
    {
        MutexLockGuard lock(mutex_);
        running_ = false;        // 运行状态标识置为false
        notEmpty_.notifyAll();   // 通知所有线程(消费者),赶紧去做完任务
    }
    // 等待所有线程关闭
    for_each(threads_.begin(),// boost::bind调用类成员函数时需要传入类成员函数指针、类对象指针...
        threads_.end(),
        boost::bind(&muduo::Thread::join, _1));
}

// 执行任务  
void ThreadPool::run(const Task& task)          //添加任务(生产者)
{
    // 如果线程池没有线程,那么直接执行任务
    // 也就是说假设没有消费者,那么生产者直接消费产品,而不把任务加入任务队列
    if (threads_.empty())                       
    {
        task();
    }
    // 如果线程池有线程,则将任务添加到任务队列  
    else
    {
        MutexLockGuard lock(mutex_);
        while (isFull())                            //任务满了阻塞等待
        {
            notFull_.wait();
        }
        assert(!isFull());

        queue_.push_back(task);                     //放进去一个任务
        notEmpty_.notify();                         //告诉take函数(消费者)现在最少有一个任务可以取走了
    }
}


// 任务分配函数(获取任务)
// 线程池函数或者线程池里面的函数都可以到这里取出一个任务
// 然后在自己的线程中执行任务,返回一个任务指针
ThreadPool::Task ThreadPool::take()
{
    MutexLockGuard lock(mutex_);
    // always use a while-loop, due to spurious wakeup
    // 任务队列为空且线程池处于运行状态,需要等待任务的到来
    while (queue_.empty() && running_)            //线程池还在运作,且没有任务要处理了就等待
    {
        notEmpty_.wait();
    }
    Task task;
    if (!queue_.empty())
    {
        // 获取任务并弹出
        task = queue_.front();
        queue_.pop_front();
        if (maxQueueSize_ > 0)                          //取走一个任务,告诉run函数(生产者)最少可以放进来一个了
        {
            notFull_.notify();
        }
    }
    return task;
}

bool ThreadPool::isFull() const
{
    mutex_.assertLocked();
    return maxQueueSize_ > 0 && queue_.size() >= maxQueueSize_;
}

void ThreadPool::runInThread()
{
    try
    {
        if (threadInitCallback_)                //每个线程初始的回调函数
        {
            threadInitCallback_();
        }
        while (running_)                   //消费者(只要是ruinning态,就会一直尝试取任务->做任务->取任务。。。)
        {
            Task task(take());
            if (task)
            {
                task();
            }
        }
    }
    catch (const Exception& ex)
    {
        fprintf(stderr, "exception caught in ThreadPool %s\n", name_.c_str());
        fprintf(stderr, "reason: %s\n", ex.what());
        fprintf(stderr, "stack trace: %s\n", ex.stackTrace());
        abort();
    }
    catch (const std::exception& ex)
    {
        fprintf(stderr, "exception caught in ThreadPool %s\n", name_.c_str());
        fprintf(stderr, "reason: %s\n", ex.what());
        abort();
    }
    catch (...)
    {
        fprintf(stderr, "unknown exception caught in ThreadPool %s\n", name_.c_str());
        throw; // rethrow
    }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值