muduo网络库——ThreadPool

本文详细解析了Muduo库中线程池的实现,包括接口设计、成员变量、线程启停、任务存取以及子线程运行逻辑。线程池通过维护一个任务队列和一组工作线程,实现任务的高效调度和执行。在存任务时,确保队列不满,取任务时确保队列非空,同时处理虚假唤醒问题,确保线程安全。
摘要由CSDN通过智能技术生成

模型

源码分析

1)接口

class ThreadPool : noncopyable
{
 public:
  typedef std::function<void ()> Task;

  explicit ThreadPool(const string& nameArg = string("ThreadPool"));
  ~ThreadPool();

  void setMaxQueueSize(int maxSize) { maxQueueSize_ = maxSize; }  // 设置队列大小
  void setThreadInitCallback(const Task& cb)                      // 设置回调
  { threadInitCallback_ = cb; }

  void start(int numThreads);   // numThreads为要创建的子线程数目,开启线程
  void stop();                  // 关闭线程

  const string& name() const
  { return name_; }

  size_t queueSize() const;

  void run(Task f);             // 任务执行函数
};

2)成员变量

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

  mutable MutexLock mutex_;   //互斥锁
  Condition notEmpty_ GUARDED_BY(mutex_);  // 条件变量: 非空判断
  Condition notFull_ GUARDED_BY(mutex_);   // 条件变量: 非满判断
  string name_;
  Task threadInitCallback_;                // 回调函数
  std::vector<std::unique_ptr<muduo::Thread>> threads_;  // 线程组 使用智能指针维护单个线程对象
  std::deque<Task> queue_ GUARDED_BY(mutex_);  //双向队列 维护要执行的任务
  size_t maxQueueSize_;
  bool running_;  //是否运行标志 

3)线程启停

void ThreadPool::start(int numThreads)
{
  assert(threads_.empty());
  running_ = true;
  threads_.reserve(numThreads);
  for (int i = 0; i < numThreads; ++i)
  {
    char id[32];
    snprintf(id, sizeof id, "%d", i+1);
    threads_.emplace_back(new muduo::Thread(
          std::bind(&ThreadPool::runInThread, this), name_+id)); //这里使用emplace_back(c++11)是为了提高插入性能,对比push_back
    threads_[i]->start();   // 开启numThreads个线程加入线程池,等待有任务时进行调度
  }
  if (numThreads == 0 && threadInitCallback_)
  {
    threadInitCallback_();
  }
}

void ThreadPool::stop()
{
  {
  MutexLockGuard lock(mutex_);  // 线程停止时加锁
  running_ = false;
  notEmpty_.notifyAll();
  notFull_.notifyAll();
  }
  for (auto& thr : threads_)
  {
    thr->join();
  }
}

说明:线程开始时不需要加锁,是因为此时只是创建空线程,并没有具体的任务来进行调度,故为单线程,不存在数据被多线程改变的情况。stop的时候有共享数据的修改,故需要加锁。

4)存任务

//注意: 存任务时一般需要进行队列非满判断; 取任务时一般需要进行队列非空判断
void ThreadPool::run(Task task)
{
  if (threads_.empty())
  {
    task();
  }
  else
  {
    MutexLockGuard lock(mutex_);
    while (isFull() && running_)  
    {
      notFull_.wait();  
    }
    if (!running_) return;
    assert(!isFull());

    queue_.push_back(std::move(task));
    notEmpty_.notify();  //非满通知 注意这里时notify(),而不是notifyAll()
  }
}

5)取任务

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)
    {
      notFull_.notify();
    }
  }
  return task;
}

这里主要关注一个虚假唤醒的问题:

while (queue_.empty() && running_)
{
  notEmpty_.wait();
}

关于虚假唤醒,可以查看:muduo网络库——条件变量(condition variable)_却道天凉_好个秋的博客-CSDN博客

多个线程在同时等待queue不为空的条件发生。当有信号signalA通知queue不为空时,操作系统同时唤醒所有线程,如线程A竞争成功,执行下面的pop操作,使得queue再次为空。但其他线程此时也被唤醒,又由于线程A的操作导致queue再次为空,其他线程无法执行对应的操作,即被虚假唤醒。

6)子线程运行函数

void ThreadPool::runInThread()
{
  try
  {
    if (threadInitCallback_)
    {
      threadInitCallback_();
    }
    while (running_)        // 每一个线程都是一个loop 只有在stop的时候才会停止
    {
      Task task(take());    // 从队列中取任务 
      if (task)          
      {
        task();             // 如果取到任务,则执行任务内容
      }
    }
  }
  ...
}

测试

#include "muduo/base/ThreadPool.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/CurrentThread.h"
#include "muduo/base/Logging.h"

#include <stdio.h>
#include <unistd.h>

// 作为入队任务,有任务执行时打印当前线程id
void print()
{
  printf("tid=%d\n", muduo::CurrentThread::tid());
}

void test(int maxSize)
{
  LOG_WARN << "Test ThreadPool with max queue size = " << maxSize;
  muduo::ThreadPool pool("MainThreadPool");   // 创建线程池
  pool.setMaxQueueSize(maxSize);              // 设置队列最大值
  pool.start(5);                              // 开启5个子线程来进行调度

  LOG_WARN << "Adding";
  pool.run(print);  // 入队
  pool.run(print);  // 入队
  for (int i = 0; i < 100; ++i)
  {
    char buf[32];
    snprintf(buf, sizeof buf, "task %d", i);
    pool.run(std::bind(printString, std::string(buf)));
  }
  LOG_WARN << "Done";

  muduo::CountDownLatch latch(1);
  pool.run(std::bind(&muduo::CountDownLatch::countDown, &latch));
  latch.wait();
  pool.stop();
}

int main()
{
  test(0);
  test(1);
  test(5);
  test(10);
  test(50);
}

总结

线程池为避免频繁创建、销毁线程,提供一组子线程,能从工作队列取任务、执行任务,而用户可以向工作队列加入任务,从而完成用户任务。

主要步骤如下:

1)创建一组线程,每个线程为一个loop循环;

2)有任务需要执行时,入队列;

3)当有线程池中有线程空闲时,从队列中取出任务执行回调;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值