手写muduo库(EventLoopThreadPool)

1,298 阅读4分钟

EventLoopThreadPool是一个事件线程池,通过这个池来管理EventLoop,EventLoop本身绑定的就是线程

  1. EventLoopThreadPool.h
#pragma once
#include "noncopyable.h"

#include<functional>
#include<string>
#include<vector>
#include<memory>

class EventLoop;
class EventLoopThread;

class EventLoopThreadPool : noncopyable
{
public:
    using ThreadInitCallback = std::function<void(EventLoop*)>;

    EventLoopThreadPool(EventLoop* baseLoop, const std::string &nameArg);
    ~EventLoopThreadPool();

    //设置底层线程的数量,TcpServer::setThreadNum底层调用的就是EventLoopThreadPool::setThreadNum
    void setThreadNum(int numThreads) { numThreads_ = numThreads; }
    //根据指定的线程数量在池里面创建numThread_个数的事件线程
    void start(const ThreadInitCallback &cb = ThreadInitCallback());

    //如果工作在多线程中,baseLoop_会默认以轮询的方式分配channel给subloop
    EventLoop* getNextLoop();

    //返回事件循环池所有的EventLoop,就是loops_
    std::vector<EventLoop*> getAllLoops();

    bool started()const { return started_; }
    const std::string name()const { return name_; }
private:
    EventLoop* baseLoop_; //我们使用muduo编写程序的时候,就会定义一个EventLoop变量,这个变量作为TcpServer构造函数的参数,用户创建的就叫做baseLoop
    std::string name_; //线程池的名字
    bool started_;
    int numThreads_;
    int next_; //轮询的下标
    std::vector<std::unique_ptr<EventLoopThread>> threads_; //包含了创建的所有事件subloop的线程
    std::vector<EventLoop*> loops_; // 包含了所有创建的subLoop的指针,这些EventLoop对象都是栈上的(见EventLoopThread::threadFunc)
};

成员变量baseLoop_:我们使用muduo编写程序的时候,就会定义一个EventLoop变量,这个变量作为TcpServer构造函数的参数,用户创建的就叫做baseLoop

成员变量numThreads_:表示subReactor的数量。我们使用muduo编写程序的时候,就会定义一个EventLoop变量,这个变量作为TcpServer构造函数的参数,这就是baseLoop。如果我们不使用setThreadNum指定Reactor模型线程数量,那么muduo默认使用单线程模型,这个线程既负责新用户连接,也负责已连接用户的读写事件

成员变量loops_:包含了所有创建的subLoop的指针,这些EventLoop对象都是栈上的(见EventLoopThread::threadFunc),不需要我们手动释放。EventLoopThread::threadFunc函数是由线程执行的,EventLoop对象存在于线程栈上

  1. EventLoopThreadPool.cc
#include "EventLoopThreadPool.h"
#include "EventLoopThread.h"

#include<memory>

EventLoopThreadPool::EventLoopThreadPool(EventLoop* baseLoop, const std::string &nameArg)
    :baseLoop_(baseLoop)
    ,name_(nameArg)
    ,started_(false)
    ,numThreads_(0)
    ,next_(0)
{}

//析构的时候不需要关注vector析构的时候里面存的EventLoop指针执行的外部资源是否需要单独delete,
//因为线程里面绑定的loop都是栈上的对象,不用担心需要手动delete它,它也不会释放掉,
//因为它下边的loop.loop()一直在循环,这个函数不会出右括号的,除非这个EventLoop底层的poller不工作了
//出右括号这个对象会自动析构的
EventLoopThreadPool::~EventLoopThreadPool()
{}

//根据指定的线程数量在池里面创建numThread_个数的事件线程
void EventLoopThreadPool::start(const ThreadInitCallback &cb)
{
    started_ = true;
    //用户通过setThreadNum设置了线程数就会进入循环
    for(int i = 0; i < numThreads_; ++i)
    {
        char buf[name_.size() + 32];
        snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i); //底层线程名字 = 线程池名字+循环下标
        EventLoopThread *t = new EventLoopThread(cb, buf);
        threads_.push_back(std::unique_ptr<EventLoopThread>(t)); // 用unique_ptr管理堆上的EventLoopThread对象,以免我们手动释放
        loops_.push_back(t->startLoop()); //底层创建线程,绑定一个新的EventLoop,并返回该loop的地址
    }

    //整个服务端只有一个线程,运行着baseloop
    if(numThreads_ == 0 && cb)
    {
        cb(baseLoop_);
    }
}

//如果工作在多线程中,baseLoop_会默认以轮询的方式分配channel给subloop
EventLoop* EventLoopThreadPool::getNextLoop()
{
    EventLoop* loop = baseLoop_;

    if(!loops_.empty()) //通过轮询获取下一个处理事件的loop
    {
        loop = loops_[next_];
        ++next_;
        if(next_ >= loops_.size())
        {
            next_ = 0;
        }
    }
    return loop;
}

//返回事件循环池所有的EventLoop,就是loops_
std::vector<EventLoop*> EventLoopThreadPool::getAllLoops()
{
    if(loops_.empty())
    {
        return std::vector<EventLoop*>(1,baseLoop_);
    }
    return loops_;
}

为什么muduo库采用固定数量的线程池,而不采用动态增长的线程池?

因为它只是一个网络库,只负责IO线程接收新连接和工作线程监听已连接客户端发生的读写事件,而不管是什么事件,耗不耗费服务端的资源。比较耗费资源的大任务应该由业务层处理,而不在网络库的职责范围内。网络库的EventLoopThreadPool只负责比较简单的监听事件,而不去处理具体的事件,所以我们开启的线程和CPU核数相同即可,可以让线程并行

在muduo网络库中,一个线程负责一个EventLoop,会监听很多fd。如果这个工作线程还要处理A用户到B用户文件传输请求的话,那么当前这个工作线程其他用户再有什么事件发生的话,这个工作线程就无法及时处理,所以像这种有耗时的IO操作还是需要有线程专门去做的