muduo网络库:Reactor模型的介绍

首先我们先了解几个问题:

多线程能提高并发度吗?

如果指的是“并发连接数”,那么不能。

假如单纯采用 thread per connection 的模型,对于32位 linux,一个进程的地址空间是4G,其中用户态能访问3G左右,而一个线程的默认栈大小为10M,心算可以得到,一个进程大约最多能同时启动300个线程左右,这远远低于基于事件单线程程序所能轻松达到的并发连接数(几千上万,甚至几万)。所谓“基于事件”,指的是用 I/O mutiplexing event loop 的编程模型,又称为 Reactor 模式。

多线程可以提高吞吐量吗?

对于计算密集型服务,不能

如果在一个8核的机器上压缩100个 1G 的文本文件,每个core的处理能力为 200MB/s,那么“每次起8个进程,一个进程压缩一个文件”与“只启动一个进程(8个线程并发压缩一个文件)”,这两种方式总耗时相当,但是第二种方式能较快的拿到第一个压缩完的文件。

多线程如何让 I/O 和计算重叠

多线程程序如何让 I/O 和计算重叠,降低 latency (迟延)

例如:日志(logging),多个线程写日志,由于文件操作比较慢,服务线程会等在 IO上,让 CPU 空闲,增加响应时间。

解决办法:单独用一个 logging 线程负责写磁盘文件,通过 BlockingQueue 提供对外接口,别的线程要写日志的时候往队列一塞就行,这样服务线程的计算和logging线程的磁盘 I/O 就可以重叠了。

线程池大小的选择

如果池中执行任务时,密集计算所占时间比重为P(0 < P <= 1),而系统一共有 C 个CPU,为了让 C 个 CPU跑满而不过载,线程池大小的经验公式 T=C/P。

假设 C = 8,P = 0.5,线程池的任务有一半是计算,一半是 IO,那么T = 16,也就是16个“50%繁忙的线程”能让 8 个 CPU忙个不停。

线程分类

  • I/O 线程 (这里特指网络 I/O)
  • 计算线程 第三方库所用线程,
  • 如 logging

Reators + 线程池 +架构

muduo网络库:Reactor模型的介绍_第1张图片

Reactor 介绍

什么是Reactor? 换个名词“non-blocking IO + IO multiplexing”,意思就显而易见了。Reactor模式用 非阻塞IO + poll(epoll)函数来处理并发,程序的基本结构是一个事件循环,以事件驱动和事件回调的方式实现业务逻辑。

while(!done)
{
    int retval  = poll(fds,nfds,timeout)
    if(retval < 0)
        处理错误,回调用户的error handler
    else{
        处理到期的timers,回调用户的timer handler
        if(retval > 0){
            处理IO事件,毁掉用户的IO event handler
        }
    }
}

这段代码形式上非常简单,跟我上一篇文章epoll的例子十分相似,除了没有处理超时timer部分。在muduo的实现中,定时器使用了linux平台的timerfd_*系列函数, timers和其它IO统一了起来。

单线程Reactor实现

muduo的Reactor核心主要由Channel、EventLoop、Poller、TimerQueue这几个类完成。乍一看还有一点绕,代码里面各种回掉函数看起来有点不直观。另外,这几个类的生命周期也值得注意,容易理不清楚。

1:Channel 类

Channel类比较简单,负责IO事件分发,每一个Channel对象都对应了一个fd,它的核心成员如下:

  EventLoop* loop_;
  const int fd_;
  int events_;
  int revents_;
  int index_;
 
  ReadEventCallback readCallback_;
  EventCallback writeCallback_;
  EventCallback errorCallback_;
  EventCallback closeCallback_;

几个callback函数都是c++新标准里面的function对象(muduo里面是Boost::function),它们会在 handleEvent 这个成员函数中根据不同的事件被调用。index_是 poller 类中 pollfds_数组的下标。events_和 revents_明显对应了 struct pollfd 结构中的成员。需要注意的是,Channel 并不拥有该 fd,它不会在析构函数中去关闭这个 fd( fd是由Socket类的析构函数中关闭,即RAII的方法),Channel 的生命周期由其 owner 负责。

2.Poller类

Poller类在这里是poll函数的封装(在muduo源码里面是抽象基类,支持poll和epoll),它有两个核心的数据成员:

  typedef std::vector<struct pollfd> PollFdList;
  typedef std::map<int, Channel*> ChannelMap;  // fd to Channel
  PollFdList pollfds_;
  ChannelMap channels_;

ChannelMap是fd到Channel类的映射,PollFdList保存了每一个fd所关心的事件,用作参数传递到poll函数中,Channel类里面的index_即是这里的下标。Poller类有下面四个函数

Timestamp poll(int timeoutMs, ChannelList* activeChannels);
void updateChannel(Channel* channel);
void removeChannel(Channel* channel);
private:
void fillActiveChannels(int numEvents, ChannelList* activeChannels) const;

updateChannelremoveChannel 都是对上面两个数据结构的操作,poll函数是对::poll的封装。私有的 fillActiveChannels 函数负责把返回的活动时间添加到 activeChannels(vector)这个结构中,返回给用户。Poller 的职责也很简单,负责IO multiplexing,一个 EventLoop 有一个 Poller,Poller的生命周期和 EventLoop一样长

3.EventLoop类

EventLoop类是核心,大多数类都会包含一个EventLoop*的成员,因为所有的事件都会在EventLoop::loop()中通过Channel分发。先来看一下这个loop循环:

while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    //++iteration_;
    if (Logger::logLevel() <= Logger::TRACE)
    {
      printActiveChannels();
    }
    // TODO sort channel by priority
    eventHandling_ = true;
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    currentActiveChannel_ = NULL;
    eventHandling_ = false;
    doPendingFunctors();
  }

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}

handleEvent 是 Channel 类的成员函数,它会根据事件的类型去调用不同的 Callback。循环末尾还有一个doPendingFunctors(),这个函数的作用在后面多线程的部分去说明。

由上面三个类已经可以构成 Reactor 的核心,整个流程如下:

  • 用户通过 Channel 向 Poller 类注册 fd 和关心的事件
  • EventLoop 从 poll 中返回活跃的 fd 和对应的 Channel
  • 通过 Channel 去回掉相应的时间。

muduo的书里面有一个时序图(8-1),很清楚的说明了整个流程。

muduo网络库:Reactor模型的介绍_第2张图片

你可能感兴趣的:(muduo网络库)