从epoll构建muduo-13 Reactor + ThreadPool 成型

mini-muduo版本号传送门
version 0.00 从epoll构建muduo-1 mini-muduo介绍
version 0.01 从epoll构建muduo-2 最简单的epoll
version 0.02 从epoll构建muduo-3 增加第一个类,顺便介绍Reactor
version 0.03 从epoll构建muduo-4 增加Channel
version 0.04 从epoll构建muduo-5 增加Acceptor和TcpConnection
version 0.05 从epoll构建muduo-6 增加EventLoop和Epoll
version 0.06 从epoll构建muduo-7 增加IMuduoUser
version 0.07 从epoll构建muduo-8 增加发送缓冲区和接收缓冲区
version 0.08 从epoll构建muduo-9 增加onWriteComplate回调和Buffer
version 0.09 从epoll构建muduo-10 Timer定时器
version 0.11 从epoll构建muduo-11 单线程Reactor网络模型成型
version 0.12 从epoll构建muduo-12 多线程代码入场
version 0.13 从epoll构建muduo-13 Reactor + ThreadPool 成型


mini-muduo v0.13版本号,mini-muduo完整可执行演示样例可从github下载,使用命令git checkout v0.13可切换到此版本号,在线浏览此版本号到这里

本版是个里程碑版本号。能够通过本版了解多线程是怎样通过IO线程读/写网络数据的,在前一个版本号v0.12重点介绍了基础知识的前提下,本篇着重分析多线程逻辑里最重要的三个方法EventLoop::runInLoop/EventLoop::queueInLoop/EventLoop::doPendingFunctors。以下逐步介绍本版本号改动的细节。三个方法放在最后的EventLoop节。


1 Task类

    这个类是v0.13版本号新增加的,它就是一个携带參数的回调。它的作用就是闭包(closure),它是我们用来取代muduo里boost::function和boost:bind的。为什么不使用boost::function和boost:bind?之前解释过了。为了不引入新概念。减少mini-muduo的学习成本。这个Task类不太具有通用性(不像BlockingQueue,范型实现)。仅仅是为了在本项目里使用的。Task仅仅支持两种类型的回调,第一种是无參数的回调。被调用者仅仅须要实现一个”void run0()“。另外一种是有两个參数的回调,被调用者实现"void run2(const string&, void*)"。

由于有了Task类,全部须要异步回调的地方都用它来实现了。

2 TcpConnection

    加入了一个sendInLoop方法。把原来send方法里的实现移动到了sendInLoop方法里,而send方法本身变成了一个外部接口的包装。依据调用send方法所在线程的不同,採取不同的策略,假设调用TcpConnection::send的线程刚好是IO线程。则立马使用write将数据送出(当然是缓冲区为空的时候)。假设调用TcpConnection::send的线程是work线程(也就是后台处理线程)则仅仅将要发送的信息通过Task对象丢到EventLoop的异步队列中,然后立马返回。

EventLoop随后会在IO线程回调到TcpConnetion::sendInLoop方法,这样做的目的是保证网络IO相关操作仅仅在IO线程进行。

3 TimerQueue

    修改不大,仅仅是用Task包装了异步请求。这样保证全部关于Timer的操作都在IO线程进行。由于我们就是用timerfd来实现的定时器,而timerfd又是由epoll来监控的,所以这非常好理解,对epoll监控的全部文件描写叙述的操作都要放到IO线程。    

4 EchoServer

    在接到任务后不是立马处理,而是将任务通过ThreadPool::addTask丢进线程池,使用多线程处理,在真正的处理回调里,简单的模拟了一个消耗CPU的函数(计算斐波那契数列),通过log能够看到。每次的任务都被分配给了池子里的不同线程。

5 EventLoop

    1 wakeup方法的实如今上一版本号v0.12已经增加。可是调用被凝视掉了。这次调用点位于EventLoop::queueInLoop。这种方法是用唤醒IO线程的,确切的说是唤醒IO线程里的epoll_wait。仅仅有一点要注意,别忘记在EventLoop::handleRead里读出这个uint_64,否则会导致eventfd被持续激发使程序进入无限循环。

    2 EventLoop::queueInLoop方法,这种方法在v0.12版本号叫queueLoop,为了和原始muduo保持一致,本版改名了。这种方法的作用是将一个异步回调加入到待运行队列_pendingFunctors中。与v0.12版本号相比第一个差异是本版本号对_pendingFunctors加了锁,这点非常好理解,由于EventLoop::queueLoop常常被外部的其它非IO线程调用。第二个改动是加入了一定条件下的wakeup()唤醒。为什么单线程版本号没有这个唤醒逻辑?由于单线程版本号里全部的异步调用都是在Loop循环開始后,doPendingFunctors()之前触发的,仅仅须要把回调插入_pendingFunctors这个数组就可以。可是在多线程版本号queueInLoop的入口就非常多了,比方以下这3种情况下,都可能调用EventLoop::queueInLoop

        情况 1 IO线程。在IMuduoUser::onMessage回调里。比方EchoServer::onMessage里。

        情况 2 IO线程,在doPendingFunctors()运行Task->doTask的实现体里。比方EchoServer::onWriteComplate里。

        情况 3 非IO线程,线程池的还有一个线程中。

在单线程版本号里,能够不考虑情况3。情况2尽管有可能发生。可是我们当时简单如果用户仅仅会在onMessage加入Task,而不会在Task的回调里再加入Task。

所以上一个版本号在此处进行了简单化处理,并不须要wakeup()操作。

本版本号因为要考虑这几种情况,所以加入了一些条件推断和wakeup()调用。

特别要注意_callingPendingFunctors变量。这个变量有点隐晦。我開始在敲代码的时候忽略了它,后来发现它很重要,在上面的情况2时,假设没有这个变量,会导致异步调用永远不会触发!

    3 EventLoop::runInLoop方法,本版本号新加入的方法,与queueInLoop方法很相似,"runIn"和"queueIn"从名字的差异就能够理解,当外部调用runInLoop的时候,会推断当前是否为IO线程。假设是在IO线程,则立马运行Task里的回调。否则通过调用queueInLoop将Task加入到异步队列,等待兴许被调用。

    4 EventLoop::doPendingFunctors。这种方法与queueInLoop方法一样,也是两处改动。首先是因为多线程操作vector必需要加锁,另外是加入了_callingPendingFunctors变量的控制。再次强调这个变量很重要。


本篇的最后。为了更清晰的解释EventLoop在多线程环境下的逻辑,我画了一张时序图。时序图表达的就是“在非IO线程里调用TcpConnection::send发送数据”这一动作引发的连锁调用。

这一动作须要3个Loop来完毕,涉及4个子调用过程。

绿色表明代码工作在IO线程红色表示代码工作在Work线程(工作线程。真正处理计算任务的线程)。在原书中多线程EventLoop的解说位于294页附近。可是非常遗憾,作者没有为这一过程制作时序图。我这里算是补画一张吧。



原文地址:https://www.cnblogs.com/brucemengbm/p/7149480.html