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用来删除链接。