muduo -- eventloop分析

几个问题:

1.    currentActiveChannel_->handleEvent(pollReturnTime_);

       如果handleEvent内某个事件响应函数很慢或者卡住,岂不是会影响后续事件处理延迟,且影响该eventloop thread的poll执行。

 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

muduo 对虚基类接口,类似如下定义

class ICallBcak {
public:
      virtual  ConnectCb ( );
      virtual ReadCb();
};

感觉muduo作者很反感虚基类+虚继承,认为虚继承是上贼船,如果继承则必须都实现,工作量较大,较重不够轻量级

抛弃 虚接口 定义,而是使用 boost::bind( &SomeFunClass::Fun1, FunClassPtr, _1, _2, _3 ) 方式实现,很简洁,没有继承多态的顾虑

 muduo 源码中大量使用 boost::bind 替代虚函数 处理回调接口。外部调用者只需要按照回调函数的接口定义规则实现回调接口,传入muduo的导出功能类内部即可。无需考虑继承某些回调接口,导致工作量较大

1.   eventloop 启动后关注两个重要 fd ,wakeupfd_  和 timerfd_  这两个fd 都分别被 Channel 封装。

    wakeupfd_  负责在eventloop循环中被叫醒。

EventLoop中的wakeupFd_就是eventfd,wakeupChannel_和wakeupFd_相关联,EventLoop关注了wakeupChannel_的读事件,当要唤醒(即poller_->poll)时,写wakeupFd_即可。

    timerfd_ 在TimerQueue内部被创建。

    故只要一个eventloop对象启动,至少关联两个事件 timerfd 和 wakeupfd

2.  Channel 封装 fd_ 句柄 和对应的 event 事件,以及 读、写、关闭等 cb 函数

3.  eventloop 内聚合 epoll IO复用对象 boost::scoped_ptr<Poller> poller_; 这个对象一般使用new EPollPoller(loop); epoll作为事件复用引擎。这个poller在eventloop构造函数中创建,负责对上述 timerfd和wakeupfd,以及以后加入eventloop的事件进行监视和IO处理。

5 .  自己打算使用muduo的tcpserver的话,假设我们的服务类叫: CSomeTcpServer 

      只需要包含 muduo::TcpServer,而不是继承。

      CSomeTcpServer 需要传入 主线程 的 EventLoop  mainLoop 

      相当与在主线程中实现一个消息循环,类似windows的GetMessage

      5.1  主线程  使用 Acceptor 来实现服务器套接字创建,bind,listen

      5.2  Acceptor 的 listen 操作 并没有在TcpServer 的start内直接调用,而是多了一步 runInLoop 不清楚为什么不直接调用。

            RunInLoop 内会判断,如果在当前线程则可直接调用,如果是跨线程调用则,QueueInLoop 把 要执行的函数放入 EventLoop 的pendingFunctors内执行(当然了,加入未觉队列,是要加锁),且判断  如果不在当前loop线程 或者 已经正在执行 pending 则wakeupfd通知一下,让eventloop内的 epoll_wait 及时返回,开始执行 pendingFunctor

         此时,TcpServer 的 start 接口启动主线程侦听功能,而runInLoop则会判断是否当前线程是否根loop所在线程属于同一个线程,如果是则在当前线程中调用,目前看来一定是,所以在当前线程中执行Acceptor::listen

void TcpServer::start() {
if (started_.getAndSet(1) == 0) {
        threadPool_->start(threadInitCallback_);
        assert(!acceptor_->listenning());
        loop_->runInLoop(boost::bind(&Acceptor::listen, get_pointer(acceptor_)));
    }
}

      5.3  acceptor 会在TcpServer构造函数内,设置新连接到来的回调函数

acceptor_->setNewConnectionCallback(boost::bind(&TcpServer::newConnection, this, _1, _2));

       5.4  TcpServer 在主线程中被 EventLoop 卡住,而 EventLoop 中最初关注的事件至少三个,

              wakeupfd,timerfd(TimeQueue),还有侦听listen socket 任何一个有事件出发,eventloop 都会返回.

              IO eventloop 工作线程,最少关注事件对象两个: wakeupfd, timerfd

      5.5  void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr) 在主线程中,被触发调用。

             因为主线程序 使用eventloop 关注了listensocket的读事件,有连接发生就会触发 void TcpServer::newConnection 

            进而 触发 TcpServer的线程池选择工作线程逻辑,轮询round robin找到一个io工作线程的ioLoop ,然后执行

             ioLoop-  >runInLoop(boost::bind(&TcpConnection::connectEstablished, conn)); 要求在io工作线程内执行 connectEstablished

runInLoop重要函数

在IO线程内执行某个用户任务的回调,即void EventLoop::runInLoop(const Functor& cb),其中Functor是typedef boost::function<void()> Functor;

如果用户在当前IO线程调用这个函数,回调会同步进行;

如果用户在其他线程调用runInLoop,cb会被加入队列,IO线程会被唤醒来调用这个Functor。
     有了这个功能,就能轻易地在线程间调度任务,比如将某个函数调用移入其IO线程,这样可以在不用锁的情况下保证线程安全性!(这样,到目前为止EventLoop中存在两类情况,有些操作必须在一个线程中进行,比如实例化EventLoop的线程和执行loop(),执行updateChannel,removeChannel操作的线程都必须在一个线程进行,对于在别线程执行的函数,可以通过runInLoop函数执行,插入iothread的任务队列,进入pendingFunctors_.push_back(cb);)

      进入pendingFunctors_的函数需要唤醒执行,

runInLoop的实现:需要使用eventfd唤醒的两种情况 (1) 调用queueInLoop的线程不是当前IO线程。(2)是当前IO线程并且正在调用pendingFunctor。

6.  每接受一个新的tcp连接,实例化一个新的TcpConnection,这个新连接对象通过RoundRobin发送给一个工作线程。

      注意: 每个tcpserver内都有一个 EventThreadPool,实现的线程池,每个连接,会被派发到一个eventloop内,也就是一个eventloop会包含多个tcpconnection,而一个tcpconnection 只对应一个 eventloop。

     每个 TcpConnection 会设置回调函数 包括(连接回调,接受消息回调,写数据回调,关闭连接回调)。

    ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));

ioLoop 是TcpServer通过round robin选取的一个eventloop线程中的 eventloop 对象指针。

通过ioLoop来让connectEstablished 在属于他的IO线程中执行。

queueInLoop(Functor& cb),将cb放入队列,并在必要时唤醒IO线程。有两种情况需要唤醒IO线程: 

1. 调用 queueInLoop() 的线程不是IO线程; 
2. 调用 queueInLoop() 的线程是IO线程,而此时正在调用pengding functor。

当一个fd想要注册可读事件时,首先通过

Channel::enableReading()-> 
Channel::update(this)-> 
EventLoop::updateChannel(Channel)-> 
Poller::updateChannel(Channel*)

调用链向poll系统调用的侦听事件表注册或者修改注册事件。

7.  在 muduo -- Channel分析 中分析了 EventLoop::loop 内执行逻辑

loop线程在poller_->poll等待监听事件到来,当poller_->poll返回,激活事件放到activeChannel中,随后遍历处理激活事件。

void EventLoop::loop() {
...
while ( !quit_ ) {
...
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);

        eventHandling_ = true;
        for (ChannelList::iterator it = activeChannels_.begin(); it != activeChannels_.end(); ++it) {
            currentActiveChannel_ = *it;
            currentActiveChannel_->handleEvent(pollReturnTime_);
        }
        currentActiveChannel_ = NULL;
        eventHandling_ = false;
 
        //执行等待队列中的回调函数
        doPendingFunctors();
}

最后面调用doPendingFunctors()是执行pendingFunctors_中的任务

void EventLoop::doPendingFunctors()
{
    std::vector<Functor> functors;
    callingPendingFunctors_ = true;

    {
        // 这里使用了缩减临界区代码的技巧,减少锁的争用
        // 相当于剪切操作
        // 这里之所以会出现并发的问题,是因为本函数不会跨线程
        // 但是runInLoop可以跨函数,会更改pendingFunctors_
        // 所以这里需要加锁
        MutexLockGuard lock(mutex_);
        functors.swap(pendingFunctors_);
    }

    char tmpout[ 2048 ] = { 0 };
    if ( functors.size() )
        sprintf( tmpout, "doPendingFunctors work functors_size=%ld", functors.size() );
    else
        sprintf( tmpout, "doPendingFunctors work size==0" );
//    LOG_INFO << tmpout;

    for (size_t i = 0; i < functors.size(); ++i) {
        functors[i]();
    }

    // callingPendingFunctors_是个标示,表示EventLoop是否在处理任务
    callingPendingFunctors_ = false;
}

 

 

EventLoopThreadPool

///

TcpConnection抽象一个TCP连接,无论是客户端还是服务器只要建立了网络连接就会使用TcpConnection;

Connector/Acceptor分别包装TCP客户端和服务器的建立连接/接受连接;

EventLoop是一个主控类,是一个事件发生器,它驱动Poller产生/发现事件,然后将事件派发到Channel处理;

EventLoopThread是一个带有EventLoop的线程;EventLoopThreadPool自然是一个EventLoopThread的资源池,维护一堆EventLoopThread。

///服务器接收连接

服务器接收连接的实现在一个网络库中比较重要。muduo中通过Acceptor类来接收连接。在TcpClient中,其Connector通过一个关心Channel可写的事件来通过连接已建立;在Acceptor中则是通过一个Channel可读的事件来表示有新的连接到来:

Acceptor::Acceptor(....) {
        ...
        acceptChannel_.setReadCallback(
            boost::bind(&Acceptor::handleRead, this));
        ... 
    }

    void Acceptor::handleRead()
    {
        ...
        int connfd = acceptSocket_.accept(&peerAddr); // 接收连接获得一个新的socket
        if (connfd >= 0)
        {
            ...
            newConnectionCallback_(connfd, peerAddr); // 回调到TcpServer::newConnection
TcpServer::newConnection中建立一个TcpConnection,并将其附加到一个EventLoopThread中,简单来说就是给其配置一个线程:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
    {
        ...
        EventLoop* ioLoop = threadPool_->getNextLoop();
        TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                                connName,
                                                sockfd,
                                                localAddr,
                                                peerAddr));
        connections_[connName] = conn;
        ...
        ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
IO的驱动
之前提到,一旦要关心某IO事件了,就调用Channel::enableXXX,这个如何实现的呢?

class Channel {
        ...
        void enableReading() { events_ |= kReadEvent; update(); }
        void enableWriting() { events_ |= kWriteEvent; update(); }
       
    void Channel::update()
    {
        loop_->updateChannel(this);
    }


    void EventLoop::updateChannel(Channel* channel)
    {
        ...
        poller_->updateChannel(channel);
    }
最终调用到Poller::upateChannel。muduo中有两个Poller的实现,分别是Poll和EPoll,可以选择简单的Poll来看:

void PollPoller::updateChannel(Channel* channel)
    {
      ...
      if (channel->index() < 0)
      {
        // a new one, add to pollfds_
        assert(channels_.find(channel->fd()) == channels_.end());
        struct pollfd pfd;
        pfd.fd = channel->fd();
        pfd.events = static_cast<short>(channel->events()); // 也就是Channel::enableXXX操作的那个events_
        pfd.revents = 0;
        pollfds_.push_back(pfd); // 加入一个新的pollfd
        int idx = static_cast<int>(pollfds_.size())-1;
        channel->set_index(idx);
        channels_[pfd.fd] = channel;
可见Poller就是把Channel关心的IO事件转换为OS提供的IO模型数据结构上。通过查看关键的pollfds_的使用,可以发现其主要是在Poller::poll接口里。这个接口会在EventLoop的主循环中不断调用:

void EventLoop::loop()
    {
      ...
      while (!quit_)
      {
        activeChannels_.clear();
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
        ...
        for (ChannelList::iterator it = activeChannels_.begin();
            it != activeChannels_.end(); ++it)
        {
          currentActiveChannel_ = *it;
          currentActiveChannel_->handleEvent(pollReturnTime_); // 获得IO事件,通知各注册回调
        }
整个流程可总结为:各Channel内部会把自己关心的事件告诉给Poller,Poller由EventLoop驱动检测IO,然后返回哪些Channel发生了事件,EventLoop再驱动这些Channel调用各注册回调。

从这个过程中可以看出,EventLoop就是一个事件产生器。

线程模型
在muduo的服务器中,muduo的线程模型是怎样的呢?它如何通过线程来支撑高并发呢?其实很简单,它为每一个线程配置了一个EventLoop,这个线程同时被附加了若干个网络连接,这个EventLoop服务于这些网络连接,为这些连接收集并派发IO事件。

回到TcpServer::newConnection中:
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
    {
      ...
      EventLoop* ioLoop = threadPool_->getNextLoop();
      ...
      TcpConnectionPtr conn(new TcpConnection(ioLoop, // 使用这个选择到的线程中的EventLoop
                                              connName,
                                              sockfd,
                                              localAddr,
                                              peerAddr));
      ...
      ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
注意TcpConnection::connectEstablished是如何通过Channel注册关心的IO事件到ioLoop的。

极端来说,muduo的每一个连接线程可以只为一个网络连接服务,这就有点类似于thread per connection模型了。

具体关键调用逻辑 

建立连接

TcpClient::connect 
        -> Connector::start 
            -> EventLoop::runInLoop(Connector::startInLoop...
            -> Connector::connect
EventLoop::runInLoop接口用于在this所在的线程运行某个函数,这个后面看下EventLoop的实现就可以了解。 网络连接的最终建立是在Connector::connect中实现,建立连接之后会创建一个Channel来代表这个socket,并且绑定事件监听接口。最后最重要的是,调用Channel::enableWriting。Channel有一系列的enableXX接口,这些接口用于标识自己关心某IO事件。后面会看到他们的实现。
Connector监听的主要事件无非就是连接已建立,用它监听读数据/写数据事件也不符合设计。TcpConnection才是做这种事的。

客户端收发数据
当Connector发现连接真正建立好后,会回调到TcpClient::newConnection,在TcpClient构造函数中:

connector_->setNewConnectionCallback(
      boost::bind(&TcpClient::newConnection, this, _1));
TcpClient::newConnection中创建一个TcpConnection来代表这个连接:

TcpConnectionPtr conn(new TcpConnection(loop_,
                                            connName,
                                            sockfd,
                                            localAddr,
                                            peerAddr));

    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);
    ...
    conn->connectEstablished();
并同时设置事件回调,以上设置的回调都是应用层(即库的使用者)的接口。每一个TcpConnection都有一个Channel,毕竟每一个网络连接都对应了一个socket fd。在TcpConnection构造函数中创建了一个Channel,并设置事件回调函数。

TcpConnection::connectEstablished函数最主要的是通知Channel自己开始关心IO读取事件:
void TcpConnection::connectEstablished()
    {
        ...
        channel_->enableReading();
这是自此我们看到的第二个Channel::enableXXX接口,这些接口是如何实现关心IO事件的呢?这个后面讲到。

muduo的数据发送都是通过TcpConnection::send完成,这个就是一般网络库中在不使用OS的异步IO情况下的实现:缓存应用层传递过来的数据,在IO设备可写的情况下尽量写入数据。这个主要实现在TcpConnection::sendInLoop中。

TcpConnection::sendInLoop(....) {
        ...
        // if no thing in output queue, try writing directly
        if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)  // 设备可写且没有缓存时立即写入
        { 
            nwrote = sockets::write(channel_->fd(), data, len);
        }
        ...
        // 否则加入数据到缓存,等待IO可写时再写
        outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
        if (!channel_->isWriting())
        {
            // 注册关心IO写事件,Poller就会对写做检测
            channel_->enableWriting();
        }
        ...     
    }
当IO可写时,Channel就会回调TcpConnection::handleWrite(构造函数中注册)

void TcpConnection::handleWrite()
    {
        ...
        if (channel_->isWriting())
        {
            ssize_t n = sockets::write(channel_->fd(),
                               outputBuffer_.peek(),
                               outputBuffer_.readableBytes());
服务器端的数据收发同客户端机制一致,不同的是连接(TcpConnection)的建立方式不同。

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值