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
}
}