muduo源码阅读(9): EventLoop事件循环相关类

(一)TCP网络编程的本质:三个半事件

1.连接的建立,包括服务端接受(accept) 新连接和客户端成功发起(connect) 连接。TCP 连接一旦建立,客户端和服务端是平等的,可以各自收发数据。

2.连接的断开,包括主动断开(close 或shutdown) 和被动断开(read(2) 返回0)。

3.消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。

3.5 消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里“发送完毕”是指将数据写入操作系统的缓冲区,将由TCP 协议栈负责数据的发送与重传,不代表对方已经收到数据。

这其中,最主要的便是第三点: 消息到达,文件描述符可读。下面我们来仔细分析(顺便分析消息发送完毕):

在这里插入图片描述
(1)消息到达,文件可读:

内核接收-> 网络库可读事件触发–> 将数据从内核转至应用缓冲区(并且回调函数OnMessage根据协议判断是否是完整的数据包,如果不是立即返回)–>如果完整就取出读走、解包、处理、发送(read decode compute encode write)

(2)消息发送完毕:

应用缓冲区–>内核缓冲区(可全填)—>触发发送完成的事件,回调Onwrite。如果内核缓冲区不足以容纳数据(高流量的服务),要把数据追加到应用层发送缓冲区中内核数据发送之后,触发socket可写事件,应用层–>内核;当全发送至内核时,又会回调Onwrite(可继续写)

(二)事件循环类图

在这里插入图片描述

  1. 实心菱形是组合(强依赖),空心菱形是聚合(弱依赖)聚合与组合
  2. 白色类图对外可见,灰色类对外不可见
  3. Poller是IO复用的抽象类,他有3个纯虚函数,两个子类内部分别封装了poll和epoll,这个类也是muduo库唯一使用面向对象思想封装的类。Poller生存期由Eventloop控制
  4. Channel是对IO事件注册与响应的封装。他的update负责注册或更新IO可读可写等事件(Channel中的update回调Eventloop中的updateChannel,它再调Poller中的updateChannel,相当于将channel注册到Poller当中,或者说将文件描述符中可读可写事件注册到Poller中)。handleEvent函数负责对所发生的事件进行处理
  5. 一个Eventloop包含多个Channel(聚合关系,Eventloop不负责Channel的生存期的控制),也就是他可以用来捕捉多个通道的可读可写事件。Channel不拥有文件描述符,Channel对象销毁的时候不close(fd_)。fd_关联FileDescriptor, 一个Eventloop有多个FileDescriptor,一个Channel对应一个FileDescriptor。FileDescriptor是被Socket所拥有的,FileDescriptor的生存期由Socket来控制
  6. TcpConnection, Acceptor(被动,关注监听套接字的可读事件)和Connetcor(主动,关注监听套接字的可写和错误事件)包含Channel,且他们控制Channel的生存周期。Channel中的handleEvent回调handleRead和handlewrite…
  7. TcpServer和TcpClient分别包含一个Acceptor和Connector。1个TcpServer或TcpClient包含多个TcpConnection, 且他们不负责控制TcpConnection生命周期

时序图:
在这里插入图片描述

(三)函数梳理

  1. 创建事件通知描述符eventfd函数使用了flag: EFD_CLOEXEC,使用FD_CLOEXEC实现close-on-exec,关闭子进程无用文件描述符

  2. Evevtloop.h最后一个变量std::vector pendingFunctors_;比较不好理解,它是一个任务容器,存放的是将要执行的回调函数。准备这么一个容器的原因(主要为了线程安全),以及runInLoop和queueInLoop函数是如何把事件添加到这个变量里的,eventfd如何通知IO线程来处理pendingFunctors_里的函数的参考muduo网络库学习(四)事件驱动循环EventLoop介绍的很详细!

以下内容参考muduo网络库学习笔记(11):有用的runInLoop()函数

runloop函数:

代码片段1:EventLoop::runInLoop()
文件名:EventLoop.cc

// 在IO线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    // 如果是当前IO线程调用runInLoop,则同步调用cb
    cb();
  }
  else
  {
    // 如果是其它线程调用runInLoop,则异步地将cb添加到队列
    queueInLoop(cb);
  }
}

queueinloop函数:

代码片段2:EventLoop::queueInLoop()
文件名:EventLoop.cc

void EventLoop::queueInLoop(const Functor& cb)
{
  // 把任务加入到队列可能同时被多个线程调用,需要加锁
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }

  // 将cb放入队列后,我们还需要在必要的时候唤醒IO线程来处理
  // 必要的时候有两种情况:
  // 1.如果调用queueInLoop()的不是IO线程,需要唤醒
  // 2.如果在IO线程调用queueInLoop(),且此时正在调用pending functor,需要唤醒
  // 即只有在IO线程的事件回调中调用queueInLoop()才无需唤醒
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

I.第一种情况易理解:调用queueInLoop的线程不是当前IO线程时,则需要唤醒当前IO线程,
才能及时执行doPendingFunctors()。

II.第二种情况,调用queueInLoop()的线程是当前IO线程,比如在doPendingFunctors()中执行
functors[i]() 时又调用了queueInLoop()。此时doPendingFunctors() 执行functors[i]() 
过程中又添加了任务,故循环回去到poll的时候需要被唤醒返回,进而继续执行doPendingFunctors()

loop函数

代码片段3:EventLoop::loop()部分
文件名:EventLoop.cc

while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    // 执行pending Functors_中的任务回调
    // 这种设计使得IO线程也能执行一些计算任务,避免了IO线程在不忙时长期阻塞在IO multiplexing调用中
    doPendingFunctors();
  }

eventfd的好处也可见这篇文章最后(优于pipe和socketpair)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值