多线程服务器编程[3]-多线程服务器的使用场合和常用模型

本章研究对象:分布式计算的网络应用程序,基本功能可以被简单归纳为“收到数据,算一算,发出去”

单线程服务器

最常用的为“non-blocking IO + IO multiplexing”,即Reactor模式,例如

  • lighttpd
  • Nginx
  • libevent
  • Java NIO
  • Twisted(Python)

此外还有ASIO使用的Proactor模式

Reactor

结构

  • 事件循环(Event-loop)
  • 使用epoll进入阻塞监听事件,按照事件类型调用hanlder

特点

  • 需要非阻塞编程,回调函数必须是非阻塞的,只能在epoll处阻塞

    • 这导致容易割裂业务逻辑,不容易理解与维护
  • 适用于IO密集型

    • 计算太多也会阻塞
  • 虽然单线程模型不需要考虑同步问题,但不能很好的利用多核,而如果同时使用多个进程,则势必会涉及到同步问题

多线程服务器

  • 阻塞+每个请求新建一个线程
  • 阻塞+线程池
  • 非阻塞 + IO multiplexing 也即作者所称的“non-blocking IO + one loop per thread”
  • Leader/Follower等高级模式

One loop per thread

好处

  • 线程数目固定
  • 方便地在线程间调配负载
  • IO事件发生的线程是固定的,一个连接会被注册到某个线程的loop中,并一直由这个线程负责,同一个TCP连接不用考虑并发

但多线程的loop相比单线程的loop的要求变高,即需要做到线程安全,重要的问题是“如何允许一个线程往另一个线程的loop里塞东西?”

线程池

  • 适用计算任务较多的情况,重要的基础设施是BlockingQueue实现的任务队列或生产者消费者队列
  • 把计算任务或待计算数据分配给线程池中的线程

进程间通信只用TCP

其他IPC方法: pipe,FIFO,消息队列,共享内存,信号
TCP的好处:跨主机,伸缩性,双向,无负作用,可记录,可重现,容易定位故障

多线程服务器的适用场合

处理并发连接的两种方式

  • Go goroutine, python gevent等的语言提供的用户态线程,由语言的runtime自行调度

    • 廉价,可以大量创建
    • 通常配合“看起来是阻塞的IO”,这里指对于用户态的调度器阻塞,而不是对于操作系统阻塞,否则用户态多线程无法成立
  • 内核级线程配合Reactor模式,例如libevent,muduo,Netty

    • 数量与CPU数目相当
    • 非阻塞IO

由于C++并没有第一类的工具,所以本书只考虑第二类

model分析

  • 单进程+单线程 : 没有伸缩性
  • 单进程+多线程
  • 多进程+单线程

    • 复制多份单线程的进程,使用多个TCP port提供服务
    • 主进程+worker进程
  • 多进程+多线程 : 聚集了2和3的缺点

单线程

  • 需要fork的时候
  • 需要限制程序CPU占用率的时候
  • 单线程的Reactor的主要缺点是响应时间慢,多线程改善这一点

多线程

  • IO吞吐(网络/磁盘)是瓶颈时,多线程不会增加吞吐,这时候单线程+单进程就够了
  • CPU算力是瓶颈时,多线程不会增加吞吐,这时候单线程+多进程更合适也更简单
  • 对比多进程
    8核心,压缩100个文件
    多进程:每个进程压缩1个文件
    多线程:每个文件用8个线程并行压缩
    总耗时相同,因为CPU都是满载的,但是后者能更快拿到第一个压缩完的文件,这体现了多线程的低延迟
    但实际上,线程间算法的并行度很难达到100%,所以:同一个任务,多线程或许会比多进程吞吐量下降一点,但一定可以带来响应时间的提升。

多线程的好处在于

  • 提高响应速度,让IO和计算重叠
  • 相比进程每次切换都使CPU Cache失效,线程间切换成本小,因此适用于“工作集”比较大的情况
  • 分割事务,将IO线程、计算线程和第三方库(例如logging)线程分开

多线程使用BlockingQueue在进程间传递数据

  • 计算操作:一个IO线程,8个计算线程(线程池),IO线程向BlockingQueue中添加数据,计算线程收到唤醒后开始计算
  • 写操作:一个单独的logging线程,通过一个或多个BlockingQueue对外提供接口,别的线程把日志写入Queue中即可,不需要等待,这样降低了服务线程的响应时间

    • 注意,读操作并不能利用多线程的便利性,因为无论如何都需要等到结果之后才能继续进行

线程池

  • 当计算在每次响应请求中占比比较高时(20%以上)适合
  • 能简化编程;避免实时创建线程的开销;减轻IO线程的负担(IO线程进行计算的话,就不能相应IO了,会导致响应速度变差)
  • 线程池的大小需要阻抗匹配,例如8核CPU,每个计算任务线程的密集计算所占时间比重为50%,那么16个线程就能跑满全部CPU,线程池太小,会不能高效利用硬件,太大会导致额外的线程切换和内存开销

多线程不能减少工作量,而是一种统筹的思路让工作提早结束。

你可能感兴趣的