node.js 系列 7 异步IO

Posted lin_fightin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了node.js 系列 7 异步IO相关的知识,希望对你有一定的参考价值。

Node的架构分析

浏览器中的EventLoop是根据H5定义的规范实现的,不同的浏览器可能有不同的实现,而Node是有libuv实现的。
libuv是一个多平台的专注于异步IO的库
libuv中主要维护了一个Eventloop和worker threads(线程池),eventloop负责调用系统的一些其他操作,比如文件的IO,network等等。在这里插入图片描述
这就是node的架构,重要的两个部分就是v8和libuv,可以看到libuv里面的事件循环和线程池。js代码通过v8引擎,然后node的bindings最后到libuv里面去执行,libuv才是真正与操作系统交互的。

阻塞IO和异步IO

我们读取写入文件的时候,都需要继续进行系统调用(操作系统的文件系统),事实上对文件的搓操作,是一个操作系统的系统调用。node.js提供了fs模块,看似是js直接对文件进行操作,但是其实是node底层,libuv与操作系统的交互帮我们完成读取写入文件的功能。
操作系统通过提供两种调用方式:阻塞式调用和非阻塞式调用
阻塞式调用,比如读取文件,在读取结果出来前,当前线程处于阻塞态(阻塞态CPU不用分配时间片的),调用线程只有在得到结果后才能继续执行
非阻塞式调用:调用执行之后,之前的线程不会停止执行,只需要过一点时间检查一下有没有结果返回即可。

非阻塞IO的问题

我们在一开始调用时并没有获取想要的结果,意味着我们需要频繁的去查看读取到的数据是否完整,成为轮训操作。这轮训由谁完成?主线程的话不可能,太耗费性能,所以libuv提供了一个线程池

线程池

线程池会负责所有的相关的操作,并且通过轮训或者其他方式等待结果,线程池里有多个线程,当获取到结果时,就将对应的回调放到事件循环中(某一个事件队列),时间需换就可以负责后续的回调工作,告诉js应用程序我们读取完文件了,有空就来执行。线程池的操作也是通过eventloop(事件循环)来完成的,当读取到结果时,会将注册时的回调函数放到事件队列里面去。在这里插入图片描述

小结

node的架构主要是js通过v8引擎解析,一些像fs读取文件的操作通过node的bingdings交到libuv去执行,libuv主要实现了一个事件循环还有一个线程池,线程池负责操作非阻塞的操作,通过事件循环。libuv才是最主要的跟操作系统交互的部分,js读写文件都是通过底层的libuv完成的。

阻塞和非阻塞,同步和异步的区别?

概念不同,阻塞和非阻塞是对于被调用者的,比如操作系统提供了阻塞和非阻塞,对我们来说他们都是系统调用、而同步和异步是对我们来说的,对于调用者的,比如我们发起调用,需要等待结果才能继续执行,这就叫做同步调用,另一个就叫做异步调用。
而libuv就是采用非阻塞异步IO的调用方式。

Node事件循环的阶段

事件循环就像一个桥梁,连着应用程序的js和系统调用的通道,无论是setTimeout,文件IO,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环中(任务队列),事件循环会不断从任务队列中取出对应的事件来执行。
但是node的事件循环比浏览器的稍微复杂点
在这里插入图片描述
一次完整的事件循环分成很多阶段,定时器阶段主要是执行定时器函数的,待定回调阶段,比如网络请求出错。轮询阶段,线程池完成一些操作后的回调函数在这里运行,比如fs读写文件的回调函数就在这里执行,检测阶段,setImmediate(),还有关闭 的回调函数等等
node也有微任务,宏仁无,比如setTimeout IO close setImmediate这些,promise,process.nextTick等等这些也是微任务。
Node的队列是非常多的,一次事件循环(tick)需要执行很多队列,
1 nextticks队列
2 其他微任务队列
3 itmers队列,比如setTimeout这些
4 io队列 比如文件的读写的回调函数
5 setimmediate队列,
6 close队列
io的话他是如果准备好了才会插入某个tick中,去执行
要注意,比如两个setTimeout,一个0,一个500毫秒后执行,还有一个setImmediate,0的setTimeout肯定是在这次事件循环中被执行,比settimemediate先,而500的那个setTimeout还没到时间的时候,就会先执行settimmediate,因为此时500的settimeout还没加入time队列,当执行完settimemediate时,事件循环还不会结束,他会等settimeout的延时到了之后加入timer队列然后执行,所以正确的执行顺序是set0 settimmediate set500

settimmediate与settimeout(()=>{},0)的执行顺序

事件循环在io阶段停留是非常长的,因为他想让io的回调尽早地响应,setTimeout执行的时候,是需要花费一点时间,将settimeout里面的函数提到某个树结构,然后再放入到timer队列,这其中是需要花费时间的,一旦花费的时间比较长了,事件循环就会开始第一次事件循环了,第一次ticks,此时settimeout还没加入到timer对列,所以会先执行setimeediate,到第二次ticks时才执行settimeout,而当花费的时间足够,第一次ticks时,settimeout已经加入了的话,就会先执行settimeout,所以会有两种可能结果。

Stream

什么是流?
我们可以想象当从一个文件中读取数据时,文字的二进制(字节)数据会源源不断地读取到我们的程序中,而这个一连串字节,就是我们程序中的流。也可以看成是连续字节的一种表现形式和抽概念。可读,也可写。我们之前学习读写文件的时候,采用readFile这些,虽然简单,但是无法控制细节,比如控制读取的大小,暂停读取。所以通过流可以让我们控制更多的细节。
像node很多对象其实是基于流的,比如http的requesst,response。porcess.stdout对象,这些,所以的流导师EventEmitter的实例。
在这里插入图片描述
可以看下源码,这不就是es5的继承吗,直接stream继承ee,使用call继承实例,ObjectSetprototypeof继承ee的原型。
Node中有四种基本流类型,Writable(可与向其写入数据的流),Readable:可以从中读取数据的流。
在这里插入图片描述
传统的读取文件,
在这里插入图片描述
在这里插入图片描述
发布会一个stream在这里插入图片描述
这个需要自己监听回调函数,比如监听data的意思就是是等读取到有数据的时候触发。在这里插入图片描述
每次读2个,所以分成了两次,34, 56
在这里插入图片描述
可以暂停。等等。
写入
在这里插入图片描述
一次性写入,要么覆盖,要么追加,现在使用流来控制
在这里插入图片描述
在这里插入图片描述
使用write方法写入,可以写很多次,但是如果我们只是写入的话,还要记得关闭文件,在这里插入图片描述
也可以write.end()
我们学过node的http模块应该知道,res.end(xx)是返回结果,并且关闭流。
在这里插入图片描述
在这里插入图片描述
文件关闭并且还能最后写入。

pipe方法的使用

在这里插入图片描述

以上是关于node.js 系列 7 异步IO的主要内容,如果未能解决你的问题,请参考以下文章

node js异步IO机制

Node.js异步IO

现在 Node.js 添加了 io.js,是否比以前更异步了?版本怎么了?

Node.js+Socket.io+MongoDB webapps 是如何真正异步的?

Node JS、Socket.io、异步和阻塞事件循环

深入理解 python3.4 中 Asyncio 库与 Node.js 的异步 IO 机制