Linux 多线程服务端编程读书笔记(六)

Linux多线程服务端编程笔记(六)

从这一章开始开始探究muduo网络库部分

第六章 muduo网络库简介

1、 muduo网络库的一些介绍
  1. muduo 是基于 Reactor 模式的网络库,其核心是个事件循环 EventLoop,用于响应计时器和 IO 事件。
  2. muduo 采用基于对象(object- based)而非面向对象( objectoriented)的设计风格,其事件回调接口多以 boost:: function+boost:: bind 表达,用户在使用 muduo 的时候不需要继承其中的 class。
  3. muduo 的线程模型:one loop per thread + thread pool 模型。
  4. muduo中的poller是PollPoller和EPollPoller的基类,采用电平触发
2、TCP网络编程的最本质的三个半事件
  1. 连接的建立。服务端接受(accept),客户端成功发起(connect)

  2. 连接的断开。主动断开和被动断开。

  3. 消息到达。文件描述符可读。决定了网络编程风格(阻塞非阻塞,如何分包,应用层缓冲如何设计deng)

  4. 消息发送完毕,这算半个

    这里有许多难点,许多的细节需要注意,待研究完源码,看看muduo是如何解决的,后会给出连接

3、常见的并发服务器设计方案

  1. 方案0:不是一个并发服务器是一个迭代服务器,他一次只能服务一个客户,不适合长连接,适合dattime这种write-only服务。他的缺点就是无法充分利用多核,不适合执行时间较长的事件

  2. 方案1::这是传统的 Unix 并发网络编程方案,这种方案适合并发连接数不大的情况。这种方案适合“计算响应的工作量远大于 fork() 的开销”这种情况,比如数据库服务器。这种方案适合长连接,但不太适合短连接,因为 fork() 开销大于求解 sudoku 的用时。

  3. 方案2:这是传统的 Java 网络编程方案 thread-per-connection,在 Java 1.4 引入 NIO 之前,Java 网络服务程序多采用这种方案。它的初始化开销比方案 1 要小很多。这种方案的伸缩性受到线程数的限制,一两百个还行,几千个的话对操作系统的 scheduler 恐怕是个不小的负担。

  4. 方案3:这是针对方案 1 的优化,UNP 详细分析了几种变化,包括对 accept 惊群问题的考虑参考

    • 惊群现象(thundering herd)就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。网络模型如下图所示:

      Linux 多线程服务端编程读书笔记(六)_第1张图片

    • 解决方法

      1. 在Linux2.6版本以后,内核内核已经解决了accept()函数的“惊群”问题,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。所以,如果服务器采用accept阻塞调用方式,在最新的Linux系统上,已经没有“惊群”的问题了。
      2. 对于实际工程中常见的服务器程序,大都使用select、poll或epoll机制,此时,服务器不是阻塞在accept,而是阻塞在select、poll或epoll_wait。新版本的的解决方案也是只会唤醒等待队列上的第一个进程或线程
      3. Nginx中使用mutex互斥锁解决这个问题,具体措施有使用全局互斥锁,每个子进程在epoll_wait()之前先去申请锁,申请到则继续处理,获取不到则等待,并设置了一个负载均衡的算法(当某一个子进程的任务量达到总设置量的7/8时,则不会再尝试去申请锁)来均衡各个进程的任务量。后面深入学习一下Nginx的惊群处理过程。
  5. 方案4 :这是对方案 2 的优化,UNP 详细分析了它的几种变化。比第2中相比减少了创建线程的开销

以上几种方案都是阻塞式网络编程,程序(thread-of-control)通常阻塞在 read() 上,等待数据到达。但是 TCP 是个全双工协议,同时支持 read() 和 write() 操作,当一个线程/进程阻塞在 read() 上,但程序又想给这个 TCP 连接发数据,那该怎么办?

  • 一种方法是用两个线程/进程,一个负责读,一个负责写。UNP 也在实现 echo client 时介绍了这种方案
  • 另一种方法是使用 IO multiplexing,也就是 select/poll/epoll/kqueue 这一系列的“多路选择器”,让一个 thread-of-control 能处理多个连接。“IO 复用”其实复用的不是 IO 连接,而是复用线程。使用 select/poll 几乎肯定要配合 non-blocking IO,而使用 non-blocking IO 肯定要使用应用层 buffer
  1. 方案5:基本的单线程 reactor 方案,这种方案的优点是由网络库搞定数据收发,程序只关心业务逻辑;缺点在前面已经谈了:适合 IO 密集的应用,不太适合 CPU 密集的应用,因为较难发挥多核的威力。

    • 并发处理多个请求,实际上是在一个线程中完成。无法充分利用多核CPU
    • 不适合处理执行时间较长的服务,所以为了让客户感觉是在并发处理而不是循环处理,每个请求必须在相对较小的时间内执行

  1. 方案6:这是一个过渡方案,收到 Sudoku 请求之后,不在 reactor 线程计算,而是创建一个新线程去计算,以充分利用多核 CPU。这是非常初级的多线程应用,因为它为每个请求(而不是每个连接)创建了一个新线程。这个开销可以用线程池来避免,即方案 8

  2. 方案7:为了让返回结果的顺序确定,我们可以为每个连接创建一个计算线程,每个连接上的请求固定发给同一个线程去算,先到先得。这也是一个过渡方案,因为并发连接数受限于线程数目,这个方案或许还不如直接使用阻塞 IO 的 thread-per-connection 方案2。方案 7 与方案 6 的另外一个区别是一个 client 的最大 CPU 占用率,在方案 6 中,一个 connection 上发来的一长串突发请求(burst requests) 可以占满全部 8 个 core;而在方案 7 中,由于每个连接上的请求固定由同一个线程处理,那么它最多占用 12.5% 的 CPU 资源。这两种方案各有优劣,取决于应用场景的需要,到底是公平性重要还是突发性能重要。这个区别在方案 8 和方案 9 中同样存在,需要根据应用来取舍。

  3. 方案8:为了弥补方案 6 中为每个请求创建线程的缺陷,我们使用固定大小线程池,程序结构如下图。全部的 IO 工作都在一个 reactor 线程完成,而计算任务交给 thread pool。如果计算任务彼此独立,而且 IO 的压力不大,那么这种方案是非常适用的。Sudoku Solver 正好符合

  4. 方案9:这是 muduo 内置的多线程方案,也是 Netty 内置的多线程方案。这种方案的特点是 one loop per thread,有一个 main reactor 负责 accept 连接,然后把连接挂在某个 sub reactor 中(muduo 采用 round-robin 的方式来选择 sub reactor),这样该连接的所有操作都在那个 sub reactor 所处的线程中完成。多个连接可能被分派到多个线程中,以充分利用 CPU。Muduo 采用的是固定大小的 reactor pool,池子的大小通常根据 CPU 核数确定,也就是说线程数是固定的,这样程序的总体处理能力不会随连接数增加而下降。另外,由于一个连接完全由一个线程管理,那么请求的顺序性有保证,突发请求也不会占满全部 8 个核(如果需要优化突发请求,可以考虑方案 10)。这种方案把 IO 分派给多个线程,防止出现一个 reactor 的处理能力饱和。与方案 8 的线程池相比,方案 9 减少了进出 thread pool 的两次上下文切换。我认为这是一个适应性很强的多线程 IO 模型,因此把它作为 muduo 的默认线程模型。

参考

  1. 《Linux多线程服务端编程》
  2. muduo网络编程库学习

你可能感兴趣的:(muduo,网络编程)