概述
主从Reactor服务器架构中,主Reactor和子Reactor会将接收到的新连接和就绪任务压入到任务池中,以便相应的线程处理这些任务,本文主要分析主Reactor和子Reactor压入任务池的逻辑是如何实现的,以及为什么这样设计。
主Reactor任务池分析
**主Reactor的Acceptor:**主Reactor通过Acceptor来监听新的连接请求,当有新连接请求的时候,使用回调函数创建一个连接对象,并将其交给一个子Reactor来处理
**新连接压入任务池:**主Reactor将新接收的连接分配给一个子Reactor,主Reactor就是将该连接放入子Reactor任务池中
主Reactor的线程池是一群子Reactor组成主Reactor在主从Reactor模式架构中,只负责接收新连接,然后将这些新连接分发给子Reactor,而子Reactor则是负责处理这些连接上具体发生的读写事件,所以主Reactor的任务本质就是封装连接然后交给子Reactor去处理
注意,在该架构中,每个Reactor都是都自己独立的线程,切拥有自己的独立的EventLoop
子Reactor任务池分析
**子Reactor事件循环(EventLoop):**首先每个子线程拥有自己的Reactor,其次通过该Reactor监听和处理事件,监听以及处理事件都是在该线程下完成的
**就绪任务压入任务池:**子Reactor中poller检测到某个连接有数据可读、可写或者发生错误等的时候,就会调用相应的回调函数(该回调函数是在建立连接的时候就设置好的),这些回调函数就会将需要处理的任务压入到子Reactor的任务池中
**任务处理:**子Reactor会从任务池中取出任务并执行,任务池处理任务的时候,子Reactor是直接调用对应的回调函数,对这些任务进行处理,例如读取数据、发送数据
**事件处理优先级:**优先处理简单I/O事件,对于那些超时销毁任务以及其他错误等待连接都放到任务队列中执行
子Reactor的任务池是一个任务队列,存储这需要执行的任务子Reactor接收到主Reactor交付给自己的连接后,开始监听事件的发生
当监听的事件发生后,就将具体的任务(比如读写任务)添加到自己的任务队列,然后一同处理这些任务
核心代码说明
- 将Poller中响应的事件,全部放入到任务队列中
- 然后将这些响应的事件,交给Channel对象处理
- 最后统一处理任务池中的所有任务
- 注意:项目服务器中设计的任务池中放的都是,在连接过程中因为各种原因出现错误连接,然后压入到任务池,等待线程在处理好预先设置回调函数后,将这些连接释放
cpp
void EventLoop::Start() {
while(1) {
std::vector<Channel *> actives;
_poller.Poll(&actives);
for (auto &channel : actives) {
channel->HandleEvent();
}
RunAllTask();
}
}
子Reactor拥有自己专属的线程,为什么不依次执行到达的任务,而是需要任务池呢?
子Reactor单线程运行,任务池存在的最大意义就是线程安全和性能问题,设计任务池不仅可以延迟任务执行,确保线程安全,同时还可以将错误连接以及等待释放的连接合并进行处理,最后达到最终目的,安全和性能。
首先分析一下任务延迟执行的意义,例如在处理一个新连接的时候,该连接处理自身任务所需的时间过多,那么如果处理它就会导致后面的连接处理阻塞,所以将这个连接先放入任务池中,等待下一次事件循环的时候再去执行它。目的在于避免当前任务处理中引起阻塞或者递归调用,从而影响其他连接的处理。
**其次是为了多线程下的线程安全,**虽然子Reactor是在单线程下使用的,但是不可避免的可能会出现线程触发,就比如正在执行任务的时候,主线程唤醒了这个子线程来执行新连接,此时就会导致该线程处理任务不安全。所以为了线程间为了安全的传递任务,那么就需要一个安全的任务队列。
最后效率问题,例如将新连接任务以及释放连接操作统一压入任务池中处理,这样就可以减少事件循环中的开销,最终也就提高了效率。
执行所有任务操作中,是通过mutex的方式,来保证该线程执行安全
cpp
void RunAllTask() {
std::vector<Functor> functor;
{
std::unique_lock<std::mutex> _lock(_mutex);
_tasks.swap(functor);
}
for (auto &f : functor) {
f();
}
return ;
}
子Reactor既要监控又要处理响应的任务,为什么使用任务队列不会降低效率呢
设计任务队列不会降低效率的核心在于非阻塞和异步处理机制。
**首先设计事件循环的目的就是为了快速响应事件,并将任务分割成小块处理。**这样每次事件循环的时候只需要处理一定数量的任务,这样分配就保证了事件的及时处理,不会因为某个任务执行时间过长而导致整个循环阻塞。
**其次任务队列可以实现优先级划分。**即可以快速响应的任务,交给线程直接执行即可,对于那些线程不能够及时处理的任务,则先放入任务池最后统一执行。
**最后任务队列实现了任务的批处理。**因为每次事件循环可以处理任务队列中的一批任务,而不是所有任务,这样就平衡了事件处理和每个任务处理的时间分配。
子Reactor执行任务时,主Reactor交付新连接,保证子线程安全性分析
主Reactor将新连接交给子Reactor的时候,并非让子Reactor立刻停止手中的任务进行处理,而是会将这个新连接放入子Reactor的任务队列中,然后子Reactor通过事件循环机制执行的任务队列中的任务。
主Reactor接收新连接后,主Reactor会将新连接分配给子Reactor,并将处理新连接的任务放入到子Reactor的任务队列中去。
子Reactor处理任务,子Reactor在EventLoop逻辑中循环处理自己的任务队列,当处理完当前任务后,从任务队列中取出主Reactor交付给自己的新连接任务进行处理。
通过任务队列的方式,有效保证了子线程执行过程中的线程安全。
时间轮与任务队列关系分析
每一个子Reactor都是有一个自己的时间轮,时间轮中定时任务和子Reactor处理任务两者使用的同一个线程 。时间轮的定时任务以及可读任务,都会放在该线程也就是子Reactor的任务队列中。
所以,如果需要连接在某些情况刷新了自己定时器,那么该任务就可能被放到子Reactor的任务池中进行处理。
这样结合下来,子Reactor的任务队列中,是即存在I/0事件,同时也存在定时任务。在运行任务队列中所有任务时,定时任务则使用时间轮中的定时任务回调函数逻辑处理,而普通的I/0任务,则是根据连接建立的时候设计的回调函数进行处理。