找工作小项目:day10-向服务器中加入线程池

day10-向服务器中加入线程池

之前有写过一篇文章,百行实现简易线程池,用到了很多没用过C++11的新特性,甚至于对万能引用这种东西都有了新的理解。不过写的很生疏,后续复习的时候会重写一下。
这里原本的打算是实现一个拥有多线程的Reactor模型,注意我们之前实现模型在main-Reactor(EventLoop)进行了事件处理,即loop方法。但是真正的Reactor只应该负责事件分发而不应该负责事件处理。
线程的数量一般受到CPU内核数量的影响,所以每有一种新任务就开一个新线程的方案并不适合于物理系统,一般采用固定数量的线程,然后将任务添加到任务队列,工作线程不断主动取出任务队列的任务执行。
线程中最主要的是三点,其一要使用互斥锁,防止多个请求同时读写,降低负担;其二是采用条件变量为何时轮询做出判断提高CPU利用率,其三通过更新数据前排除重复储存的方式确保数据的一致性。
那么接下来就看具体代码来

1、错误检测机制

2、地址创建

3、Socket创建

4、高并发Epoll

5、Channel

这里将原本的直接callback转变为了将callback放进线程池中,相当于添加了一个容器,将事件放进了线程池中。

void Channel::handleEvent(){
    loop->addThread(callback);
}

6、EventLoop

首先我们从目的来构思一下,一定是需要一个ThreadPool对象的,既然要将处理事件的功能丢出去,那么loop是不是需要改变,将原本的处理事件变成分发事件?那么从声明来对比一下:

//改变前
class Epoll;
class Channel;
class EventLoop
{
private:
    Epoll *ep;
    bool quit;
public:
    EventLoop();
    ~EventLoop();

    void loop();
    void updateChannel(Channel*);
};
//改变后
class Epoll;
class Channel;
class ThreadPool;
class EventLoop
{
private:
    Epoll *ep;
    ThreadPool *threadPool;
    bool quit;
public:
    EventLoop();
    ~EventLoop();

    void loop();
    void updateChannel(Channel*);

    void addThread(std::function<void()>);
};

多了一个线程池多了一个加入线程池的方法,细看addThread它是将一个函数作为参数的,他将事件直接加进去了?那么loop要干嘛呢?
还是一样构造函数构造所有新属性,析构函数析构所有在原本类中没有析构的部分。

EventLoop::EventLoop() : ep(nullptr), threadPool(nullptr), quit(false){
    ep = new Epoll();
    threadPool = new ThreadPool();
}

EventLoop::~EventLoop(){
    delete ep;
}

重头戏loop,出乎意料loop没有什么变化,而是将handleEvent做了改变,之后的方法将方法加入了线程池。

void EventLoop::loop(){
    while(!quit){
    std::vector<Channel*> chs;
        chs = ep->poll();
        for(auto it = chs.begin(); it != chs.end(); ++it){
            (*it)->handleEvent();
        }
    }
}
void EventLoop::addThread(std::function<void()> func){
    threadPool->add(func);
}

总结一下,目前框架发生了一点改变

//原本的
EventLoop::loop->Channel::handleEvent
//现在的
EventLoop::loop->Channel::handleEvent->EventLoop->addThread

7、Acceptor

8、Connection

9、Buffer

10、ThreadPool

线程池的构建需要几个部分,线程队列、事件队列、互斥锁、条件变量,这是可预见的部分,方法构造析构必不可少,并且还要有一个添加事件到线程池的方法。
那么来看一下类声明,多了一个stop,应该是个标志位,来看看实现解读一下是什么标志位。

class ThreadPool
{
private:
    std::vector<std::thread> threads;
    std::queue<std::function<void()>> tasks;
    std::mutex tasks_mtx;
    std::condition_variable cv;
    bool stop;
public:
    ThreadPool(int size = 10);
    ~ThreadPool();

    void add(std::function<void()>);

};

构造函数中使用了emplace_back,相比于push_back的优势在于emplace_back不需要复制一个出来再添加,而是直接添加,在线程队列中利用匿名函数添加每个线程执行的任务,任务分为以下几步:
1、首先使用 std::unique_lock 对 tasks_mtx(任务队列的互斥量)进行加锁
2、然后,调用 cv.wait 方法等待条件变量 cv 的通知,cv.wait 接受一个 lambda 表达式作为参数,这个 lambda 表达式返回一个布尔值,表示是否满足取出任务的条件。如果 stop 为真,或者 tasks 不为空,则满足条件;否则就等待
3、当收到通知后,线程会继续执行,首先判断 stop 是否为真并且 tasks 是否为空,如果是,则直接返回,线程退出
4、如果任务队列不为空,就从队列中取出一个任务,并将其执行。

ThreadPool::ThreadPool(int size) : stop(false){
    for(int i = 0; i < size; ++i){
        threads.emplace_back(std::thread([this](){
            while(true){
                std::function<void()> task;
                {
                    std::unique_lock<std::mutex> lock(tasks_mtx);
                    cv.wait(lock, [this](){
                        return stop || !tasks.empty();
                    });
                    if(stop && tasks.empty()) return;
                    task = tasks.front();
                    tasks.pop();
                }
                task();
            }
        }));
    }
}

首先在析构函数中有个设计,就是创建作用域,以保证在互斥锁锁定期间改变stop的值,防止在这个期间加入新的事件。
接着,通过调用 cv.notify_all() 来通知所有等待中的线程,即使它们可能还在等待条件变量 cv 的通知。这个通知告诉所有线程停止等待,因为线程池即将被销毁。
最后,在一个循环中遍历线程池中的所有线程,检查每个线程是否可被加入(joinable),如果可被加入,则调用 join() 方法等待线程执行完毕。这样做的目的是等待所有线程执行完当前的任务后再销毁线程对象,以确保线程池的安全销毁。

ThreadPool::~ThreadPool(){
    {
        std::unique_lock<std::mutex> lock(tasks_mtx);
        stop = true;
    }
    cv.notify_all();
    for(std::thread &th : threads){
        if(th.joinable())
            th.join();
    }
}

之后是添加事件到线程池,通过作用域获取互斥锁
1、接着,在加锁的情况下,判断线程池的 stop 标志是否为真,如果为真,则抛出一个 std::runtime_error 异常,表示线程池已经停止,无法再添加新的任务。如果线程池未停止,就将任务函数 func 添加到任务队列 tasks 中。
2、这里使用 emplace 方法可以直接在队列尾部构造一个新的任务对象,避免了多余的拷贝。
3、最后,在添加任务后,调用 cv.notify_one() 来通知一个等待中的线程,以便该线程可以从阻塞状态中唤醒并开始执行新添加的任务。

void ThreadPool::add(std::function<void()> func){
    {
        std::unique_lock<std::mutex> lock(tasks_mtx);
        if(stop)
            throw std::runtime_error("ThreadPool already stop, can't add task any more");
        tasks.emplace(func);
    }
    cv.notify_one();
}

有兴趣可以看看之前写的线程池百行实现线程池
目前的这个线程池仍旧是较为简单的,没有右值引用没有完美转发,性能较差,之后会进行优化。

11、服务器类

12、测试线程池

这里简单测试一下线程池是否可用,也能清楚地看出来线程池的使用方法

void print(int a, double b, const char *c, std::string d){
    std::cout << a << b << c << d << std::endl;
}

void test(){
    std::cout << "hellp" << std::endl;
}

int main(int argc, char const *argv[])
{
    ThreadPool *poll = new ThreadPool();
    std::function<void()> func = std::bind(print, 1, 3.14, "hello", std::string("world"));
    poll->add(func);
    func = test;
    poll->add(func);
    delete poll;
    return 0;
}

13、服务器

int main() {
    EventLoop *loop = new EventLoop();
    Server *server = new Server(loop);
    loop->loop();
    delete server;
    delete loop;
    return 0;
}

14、总结一下

在这里插入图片描述

现在已经基本完成了单 Reactor 多线程,即EventLoop用来分发任务,线程用来处理任务,Acceptor用来建立连接,Connection用来删除链接。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值