muduo网络框架分析
概要
muduo是一个纯异步、多线程、多路复用网络框架,主线程负责监听网络连接事件,子线程负责处理已建立的连接的网络读写事件。采用eventfd实现父、子线程之间的通信。子线程之间是不能直接进行通信的,原因在于,子线程之间互相不知道对方是否已经析构,也就是一个子线程很有可能向另外一个已经退出的子线程递交任务,这样就会造成程序崩溃。
muduo网络框架是基于生产者消费者模型的。每一个线程都维护了自己的未决队列,当另外的线程或者线程自身递交一个任务到未决队列后,会向未决队列所在线程关联的eventfd写入任意的8字节数据。这样就激活了这个线程从未决队列中取出任务进行消费。
muduo网络框架默认是采用epoll多路复用,基于水平触发, 利用struct epoll_event结构中的data数据成员ptr指针存储Channel对象。
muduo网络框架是基于对象的,利用boost::function实现多态,用boost::shared_ptr,boost::scoped_ptr,boost::weak_ptr智能指针包装裸指针,尽可能的降低内存泄露风险。以安全牺牲性能。
线程池初始化
利用条件变量+互斥体实现线程池的初始化。EventLoopThreadPool对象维护了EventLoopThread线程对象池和EventLoop对象池。每一个EventLoop对象属于确切的某个线程。主线程通过调用EventLoopThread::startLoop()方法,利用条件变量和互斥体同步等待子线程创建,并初始化EventLoop对象。当子线程初始化EventLoop对象后,向条件变量发送唤醒信号。在该条件变量上睡眠的主线程被唤醒。接着主线程将得到的EventLoop对象,返回给EventLoopThreadPool::start()函数。最终在EventLoopThreadPool::start()函数中,EventLoop对象被存入vector向量。这样EventLoop对象池就创建好了。而子线程在向条件变量发送唤醒信号后,调用了EventLoop::loop()函数,进入主循环。一旦主循环退出,整个子线程就退出了。
muduo网络框架使用原子操作来避免EventLoopThreadPool对象被多次初始化。从这点可以看出,muduo网络框架的线程池是静态的,不可伸缩的。还有一点,这些子线程是无差别的,都将作为网络i/o线程。muduo网络框架使用了__thread 关键字用于标识线程独立的全局变量,也就是每个线程一个副本。
接收器acceptor
负责监听网络连接事件,从网络协议栈的全链接队列中取出完成三次握手的连接,并得到socket文件描述符。因为采用的是水平触发,所以muduo网络框架一次只从全连接队列中取出一个连接不会出问题。只要全连接队列不为空,那么监听文件描述符的可读事件将会一直响应。这样的设计并不符合高性能网络框架的要点。
Acceptor对象隶属于TcpServer。一个TcpServer拥有一个Acceptor对象。一个Acceptor对象只负责监听一个网络接口。muduo并不支持多个网络接口的监听。当Acceptor对象获取到一个已连接的文件描述符后,就会将这个连接设置成非阻塞和修改文件状态标识为closeonexec。接着调用TcpServer注册的newConnection回调函数。
框架核心TcpServer
TcpServer对象负责muduo网络框架的初始化,包括创建线程池和连接池,并且维护连接池。一个muduo网络框架只拥有一个TcpServer对象。TcpServer对象由主线程维护。TcpServer::newConnection()函数由acceptor调用,在TcpServer::newConnection函数中,首先调用EventLoopThreadPool::getNextLoop()函数,获取一个EventLoop对象,在多线程模式下,该EventLoop对象属于某个子线程。紧接着为新获取的socket文件描述符构造一个TcpConnection对象,并将该TcpConnection对象与获取的EventLoop对象关联。这个TcpConnection对象就属于某个子线程了。接着将TcpConnection对象插入到TcpServer维护的连接池中。muduo网络框架使用了智能指针,TcpServer对象维护的连接池中的成员是boost::shared_ptr<TcpConnection>对象,这样TcpConnection只要存在于连接池中,那么对象就不会被释放。
程序自此,TcpConnection关联的socket文件描述符还没有加入epoll。通过调用TcpConnection::connectEstablished()函数,将socket文件描述符加入epoll事件监听器。执行该函数的线程一定与该TcpConnection关联的EventLoop对象所属的线程保持一致。为了做到这一点,程序中使用EventLoop::runInLoop()函数包装TcpConnection::connectEstablished()函数。在多线程模式下,这将会发生线程切换,主线程对子线程关联的EventLoop对象的未决队列上锁。主线程上锁成功,将会把TcpConnection::connectEstablished()函数对象投递到子线程的未决队列中,并且向该EventLoop对象关联的eventfd写入8字节数据,用于唤醒与该EventLoop对象关联的子线程,以让子线程尽快处理未决队列中的任务。之所以要唤醒,是因为子线程很有可能阻塞在epoll_wait函数处。只有从epoll_wait返回,要么是超时时间到了,要么是接收到新的事件时,子线程才有机会处理在未决队列中的任务。那样的话,将会大大的影响,子线程处理未决队列中的任务的速率。通过向eventfd写入8字节数据,就是为了让子线程从epoll_wait中返回。因为子线程的epoll监听的eventfd产生了读事件,则epoll_wait就会返回。
TcpServer对象也控制TcpConnection的析构,TcpServer::removeConnection()函数由TcpConnection::handleClose()函数调用,执行TcpConnection::handleClose()函数的线程一定是TcpConnection对象关联的EventLoop对象所属的线程。这里将会发生一次线程切换,子线程对主线程的EventLoop对象的未决队列进行上锁,并且把函数对象TcpServer::removeConnectionInLoop()递交给主线程的EventLoop对象的未决队列中。子线程向主线程的EventLoop对象关联的eventfd写入8字节数据,唤醒主线程执行未决队列中的任务。在TcpServer::removeConnectionInLoop()函数中,主线程把boost::shared_ptr<TcpConnection>对象从连接池中移除,为TcpConnection对象的析构做好准备。紧接着主线程调用了EventLoop::queueInLoop()函数,该函数包装了TcpConnection::connectDestroyed()函数。不知道是否有读者仔细斟酌过这行代码,为什么不用EventLoop::runInLoop()函数来包装呢?在多线程模式下,能够很容易理解的。在单线程模式下,笔者花了好一阵工夫,才理解大牛陈硕的用意。这行代码是为了避免TcpConnection对象的析构发生在Channel::handleEvent的内部。因为一个Tcpconnection对象拥有一个Channel对象,如果在Channel::handleEvent内部造成了TcpConnection对象的析构,那么channel对象也一并被析构。也就会出现访问一个已经被析构的Channel对象的可能。