一步步分析 Node.js 的异步I/O机制

栏目: Node.js · 发布时间: 5年前

内容简介:它的优秀之处并非原创,它的原创之处并非优秀。《深入浅出Node》本文章节如下图所示,阅读时间大约为10分钟~15分钟,图少字多,建议仔细阅读。

它的优秀之处并非原创,它的原创之处并非优秀。

《深入浅出Node》

本文章节如下图所示,阅读时间大约为10分钟~15分钟,图少字多,建议仔细阅读。

一步步分析 Node.js 的异步I/O机制

背景

在计算机资源中, I/OCPU计算 在硬件支持上是可以并行进行的。所以,同步编程中的I/O引起的阻塞导致后续任务(可能是CPU计算,也可能是其他I/O)的等待 会造成资源的不必要浪费

说白了明明就是硬件支持,但是软件上不支持,就是浪费。所以要做的是尽最大可能不让阻塞造成没必要的 等待

问题引入

假设我们现在拿到一组任务,其中既有I/O又有CPU计算,同时假设我们的计算机是多核的但计算机资源有限的,为了减少上述的资源浪费情况你会怎么做?

一步步分析 Node.js 的异步I/O机制

第一种方案:多线程。

通过创建多个线程来分别执行CPU计算和I/O,这样CPU计算不会被I/O阻塞了。

它有如下的缺点:

  • 硬件上:创建线程和线程上下文切换有时间开销。
  • 软件上:多线程编程模型的死锁、状态同步等问题让开发者头疼。

第二种方案:单线程 + 异步I/O

首先它可以规避上述方案的缺点。

通过事件驱动的方式,当单线程执行CPU计算,I/O通过异步来进行调用和返回结果。这样也能使I/O不阻塞CPU计算。

但是它也有缺点:

  • 单线程没法利用多核CPU的优点。(一个线程肯定没法运行在多个CPU上)
  • 线程一崩,整个程序就崩溃了。(多线程这个问题的影响很小)
  • 非阻塞I/O通过轮询实现的,轮询会消耗额外的CPU资源。

问题分解

我们将上述描述的问题进行分解,梳理思路:

  • T1 :减少I/O阻塞CPU计算的时间。
  • T2 :不要带来锁、状态同步等问题。
  • T3 :能利用多核CPU的优点。
  • T4 :不要带来更多的额外消耗。

解决问题

Node通过 异步调用+维护I/O线程池+事件循环机制 来减少或避免I/O阻塞CPU计算的时间。后面我逐步解释上述三者:

异步调用

一图以蔽之。

一步步分析 Node.js 的异步I/O机制

这里我们要把异步调用处理过程抽象到操作系统层面,我们可知:异步调用是当应用程序发起I/O调用的时候,将调用信号发给操作系统, 这时应用程序继续往下执行 ,直到操作系统完成任务之后,将数据返回,应用程序通过回调获取返回数据并在程序中执行相应的回调函数。

维护I/O线程池

我们将上述的操作系统进行剖析,其实内部是 由Node维护了一个I/O线程池

当JavaScript线程(JavaScript是单线程的我就不解释了吧)执行过程中遇到了I/O任务的地方,会进行 异步调用 ,封装参数和请求对象并将其放入线程池等待队列中等待执行。

当线程池有空余线程的时候,会让空余线程执行该I/O任务,执行完成之后,归还所占用的线程,同时 我们拿到了I/O任务的执行结果

此时异步I/O进行的流程如下图所示:

一步步分析 Node.js 的异步I/O机制

IOCP是输入输出完成端口(Input/Output Completion Port,IOCP), 是支持多个同时发生的异步I/O操作的应用程序编程接口,是一个Windows内核对象。

事件循环机制

异步任务完成了,那JavaScript线程是怎么知道的呢?

最暴力也是最直接的方式就是让CPU去轮询,即创建一个无限循环一直去检查 I/O 的完成状态。所以现在为了解决**问题T1(减少I/O阻塞CPU计算的时间。) 而导致了 问题T4(不要带来更多的额外消耗。)**的产生,因为CPU会花费额外的资源去处理状态判断和不必要的“空转”。

这里我们可抽象地理解为CPU去轮询线程池中的各线程的状态。

所以我们要通过优化 问题T4 来尽可能地减少消耗。

一个著名的优化思路就是设定一个不可能达到的理想情况,然后设计具体方法来无限逼近理想目标。这里我们要优化 问题T4 使其趋近于 问题T4 不存在。

刚刚说了 一直去检查I/O的状态 是性能最低的方案(这叫read方案)。除此之外还有如下几种方案:

  • 轮询文件描述符上的事件状态(select方案) 。但是由于它采用的是1024长度的数组来存储状态,所以最多检查1024个文件描述符,这里产生了限制性。

文件描述符是一个简单的整数,用以标明每一个被进程所打开的文件和socket。不要觉得1024很大了,在海量请求面前,真的是很小的数字。

  • 基于上述采用链表存储状态(poll方案) 。但是在文件描述符较多的时候性能低下。
  • 在进入轮询的时候如果没有检查到I/O事件的完成,则轮询进行休眠,直到事件发生将它唤醒(epoll方案) 。这是 Linux 下效率最高的I/O事件通知机制,不会造成CPU的浪费,毕竟轮询线程(其实就是JavaScript线程)已经休眠了。

下面我们通过描述 生产者/消费者 模型来梳理基于epoll的整个方案:

线程池中各线程中I/O事件的完成是 事件的生产者

JavaScript线程中的事件的回调函数则是 事件的消费者

Step1: Node的轮询机制 在轮询 I/O事件完成队列 时,发现为空(即没有任何线程完成I/O),则 Node的轮询机制 进入休眠。

Step2:I/O线程池中有部分线程完成了,发送信号(操作系统完成)唤醒 Node的轮询机制 ,从 I/O事件完成队列 里取出各完成的I/O对象,并执行相应的回调函数。

Step3:如果在某次轮询时发现 I/O事件完成队列 为空,则又进入休眠直到再次被唤醒。

上述的 Node的轮询机制 则为 事件循环即Event Loop ,而 I/O事件完成队列 也为我们常说的 事件观察者

关于这部分的更多内容可细读《深入浅出Node》第三章的3.3.2~3.3.5节。

经过 事件循环 ,我们可以得出整个异步I/O的过程了。如图所示:

一步步分析 Node.js 的异步I/O机制

结论:Node通过异步调用+维护I/O线程池+事件循环机制解决了T1问题(即减少I/O阻塞CPU计算的时间),同时也将T4问题(即不要带来更多的额外消耗)的影响降至最低,由于JavaScript执行部分始终是单线程的,所以也不存在需要锁机制和各状态同步,T2问题(即不要带来锁、状态同步等问题)也不存在了。

所以这里我们可以得知,虽然JavaScript是单线程的,但是Node是多线程的,因为要维护一个I/O线程池啊。

这里我们只讲了异步I/O的情况,当然还有非I/O的异步任务,比如setTimeout。如果你看懂了上述的事件循环,其实你就可以理解为setTimeout就是往定时器观察者(这里不是I/O观察者哦,观察者有多个)队列中插入一个事件而已,每次循环的时候判断是否到期,到期就执行。

值得注意的是:定时器观察者是一棵红黑树。

好了,最后我们就要开始解决文章开头提到的 T3问题 了:

如何利用多核CPU的优点?

这里其实要解决的是 单进程单核对于多核使用不足的问题。

废话不多说, Node用的是多进程架构,并采用Master-Worker的模式。 ,理想状态下每个进程都分配到一个专属的CPU。

一步步分析 Node.js 的异步I/O机制

主进程负责调度,工作进程做具体的工作。进程间通过IPC(进程间通信)传递数据。

但是我们要注意的是,创建工作进程(即子进程)的代价昂贵,需要至少30ms的启动时间和10MB的内存空间。所以一定要在开发的时候审慎对待。

搞清楚我们的目的:多进程是为了利用多核CPU,而不是为了解决并发。

IPC可传递句柄,这让我们可以实现多个进程监听同个端口,可实现负载均衡。具体参考《深入浅出Node》第九章。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入浅出React和Redux

深入浅出React和Redux

程墨 / 机械工业出版社 / 2017-4-28 / 69

本书作者是资深开发人员,有过多年的开发经验,总结了自己使用React和Redux的实战经验,系统分析React和Redux结合的优势,与开发技巧,为开发大型系统提供参考。主要内容包括:React的基础知识、如何设计易于维护的React组件、如何使用Redux控制数据流、React和Redux的相结合的方式、同构的React和Redux架构、React和Redux的性能优化、组件的测试等。一起来看看 《深入浅出React和Redux》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具