24. poll单线程处理所有IO事件

71 阅读5分钟

重温事件驱动

基于事件的程序设计: GUI、Web

事件驱动的好处是占用资源少,效率高,可扩展性强,是支持高性能高并发的不二之选。

GUI 设定了一系列的控件,如 Button、Label、文本框等,当我们设计基于控件的程序时,一般都会给 Button 的点击安排一个函数,类似这样:

// 按钮点击的事件处理
void onButtonClick(){	
}

设计思想是,一个无限循环的事件分发线程在后台运行,当用户在界面上产生某种操作,如点击 Button,一个事件会被产生并放置到事件队列中,这个事件会有一个类似前面的 onButtonClick 回调函数。事件分发线程的任务,就是为每个发生的事件找到对应的事件回调函数并执行它。基于事件驱动的 GUI 程序就可以完美地工作了。

还有一个类似的例子是 Web 编程领域。同样的,Web 程序会在 Web 界面上放置各种界面元素,例如 Label、文本框、按钮等,和 GUI 程序类似,给感兴趣的界面元素设计 JavaScript 回调函数,当用户操作时,对应的 JavaScript 回调函数会被执行,完成某个计算或操作。这样,一个基于事件驱动的 Web 程序就可以在浏览器中完美地工作了。

通过使用 poll、epoll 等 I/O 分发技术,可以设计出基于套接字的事件驱动程序,从而满足高性能、高并发的需求。

事件驱动模型,也被叫做反应堆模型(reactor),或者是 Event loop 模型。这个模型的核心有两点。

第一,它存在一个无限循环的事件分发线程,或者叫做 reactor 线程、Event loop 线程。这个事件分发线程的背后,就是 poll、epoll 等 I/O 分发技术的使用。

第二,所有的 I/O 操作都可以抽象成事件,每个事件必须有回调函数来处理。acceptor 上有连接建立成功、已连接套接字上发送缓冲区空出可以写、通信管道 pipe 上有数据可以读,这些都是一个个事件,通过事件分发,这些事件都可以一一被检测,并调用对应的回调函数加以处理。

几种 I/O 模型和线程模型设计

任何一个网络程序,所做的事情可以总结成下面几种:

  • read:从套接字收取数据;
  • decode:对收到的数据进行解析;
  • compute:根据解析之后的内容,进行计算和处理;
  • encode:将处理之后的结果,按照约定的格式进行编码;
  • send:最后,通过套接字把结果发送出去。

这几个过程和套接字最相关的是 read 和 send 这两种。接下来,我们总结已学过的几种支持多并发的网络编程技术,引出我们今天的话题,使用 poll 单线程处理所有 I/O。

fork

使用 fork 来创建子进程,为每个到达的客户连接服务。
image.png
随着客户数的变多,fork 的子进程也越来越多,即使客户和服务器之间的交互比较少,这样的子进程也不能被销毁,一直需要存在。使用 fork 的方式处理非常简单,它的缺点是处理效率不高,fork 子进程的开销太大。

pthread

使用 pthread_create 创建子线程,因为线程是比进程更轻量级的执行单位,所以它的效率相比 fork 的方式,有一定的提高。但每次创建一个线程的开销仍然是不小的,因此引入了线程池的概念,预先创建出一个线程池,在每次新连接达到时,从线程池挑选出一个线程为之服务,很好地解决了线程创建的开销。但这个模式还是没有解决空闲连接占用资源的问题,如果一个连接在一定时间内没有数据交互,这个连接还是要占用一定的线程资源,直到这个连接消亡为止。

image.png

single reactor thread

前面讲到,事件驱动模式是解决高性能、高并发比较好的一种模式。为什么呢?

因为这种模式符合大规模生产的需求。我们的生活中遍地都是类似的模式。比如你去咖啡店喝咖啡,你点了一杯咖啡在一旁喝着,服务员也不会管你,等你有续杯需求的时候,再去和服务员提(触发事件), 服务员满足了你的需求,你就继续可以喝着咖啡玩手机。整个柜台的服务方式就是一个事件驱动的方式。

image.png

一个 reactor 线程上同时负责分发 acceptor 的事件、已连接套接字的 I/O 事件。

single reactor thread + worker threads

但是上述的设计模式有一个问题,和 I/O 事件处理相比,应用程序的业务逻辑处理是比较耗时的,比如 XML 文件的解析、数据库记录的查找、文件资料的读取和传输、计算型工作的处理等,这些工作相对而言比较独立,它们会拖慢整个反应堆模式的执行效率。

所以将这些 decode、compute、enode 型工作放置到另外的线程池中,和反应堆线程解耦,是一个比较明智的选择。

image.png 反应堆线程只负责处理 I/O 相关的工作,业务逻辑相关的工作都被裁剪成一个一个的小任务,放到线程池里由空闲的线程来执行。当结果完成后,再交给反应堆线程,由反应堆线程通过套接字将结果发送出去。