内容简介:它的优秀之处并非原创,它的原创之处并非优秀。《深入浅出Node》本文章节如下图所示,阅读时间大约为10分钟~15分钟,图少字多,建议仔细阅读。
它的优秀之处并非原创,它的原创之处并非优秀。
《深入浅出Node》
本文章节如下图所示,阅读时间大约为10分钟~15分钟,图少字多,建议仔细阅读。
背景
在计算机资源中, I/O 和 CPU计算 在硬件支持上是可以并行进行的。所以,同步编程中的I/O引起的阻塞导致后续任务(可能是CPU计算,也可能是其他I/O)的等待 会造成资源的不必要浪费 。
说白了明明就是硬件支持,但是软件上不支持,就是浪费。所以要做的是尽最大可能不让阻塞造成没必要的 等待 。
问题引入
假设我们现在拿到一组任务,其中既有I/O又有CPU计算,同时假设我们的计算机是多核的但计算机资源有限的,为了减少上述的资源浪费情况你会怎么做?
第一种方案:多线程。
通过创建多个线程来分别执行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计算的时间。后面我逐步解释上述三者:
异步调用
一图以蔽之。
这里我们要把异步调用处理过程抽象到操作系统层面,我们可知:异步调用是当应用程序发起I/O调用的时候,将调用信号发给操作系统, 这时应用程序继续往下执行 ,直到操作系统完成任务之后,将数据返回,应用程序通过回调获取返回数据并在程序中执行相应的回调函数。
维护I/O线程池
我们将上述的操作系统进行剖析,其实内部是 由Node维护了一个I/O线程池 。
当JavaScript线程(JavaScript是单线程的我就不解释了吧)执行过程中遇到了I/O任务的地方,会进行 异步调用 ,封装参数和请求对象并将其放入线程池等待队列中等待执行。
当线程池有空余线程的时候,会让空余线程执行该I/O任务,执行完成之后,归还所占用的线程,同时 我们拿到了I/O任务的执行结果 。
此时异步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通过异步调用+维护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。
主进程负责调度,工作进程做具体的工作。进程间通过IPC(进程间通信)传递数据。
但是我们要注意的是,创建工作进程(即子进程)的代价昂贵,需要至少30ms的启动时间和10MB的内存空间。所以一定要在开发的时候审慎对待。
搞清楚我们的目的:多进程是为了利用多核CPU,而不是为了解决并发。
IPC可传递句柄,这让我们可以实现多个进程监听同个端口,可实现负载均衡。具体参考《深入浅出Node》第九章。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Netty 之异步通知机制
- JavaScript 同步和异步(执行机制)
- ThinkAdmin v5.0 增加自定异步任务机制
- JavaScript异步机制(二)之任务队列和事件循环
- JS核心知识点梳理——异步,单线程,运行机制
- Vue你不得不知道的异步更新机制和nextTick原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入浅出React和Redux
程墨 / 机械工业出版社 / 2017-4-28 / 69
本书作者是资深开发人员,有过多年的开发经验,总结了自己使用React和Redux的实战经验,系统分析React和Redux结合的优势,与开发技巧,为开发大型系统提供参考。主要内容包括:React的基础知识、如何设计易于维护的React组件、如何使用Redux控制数据流、React和Redux的相结合的方式、同构的React和Redux架构、React和Redux的性能优化、组件的测试等。一起来看看 《深入浅出React和Redux》 这本书的介绍吧!