深入浅出Nodejs学习笔记——第三章 异步IO


异步I/O与非阻塞I/O

异步很难,很不好理解 如果有错,请评论指出

同步异步,阻塞与非阻塞的概念不理解 阻塞与非阻塞的概念 本文章先咱不谈异步与非阻塞的概念

首先得理解Node的单线程

Node的单线程 是单线程,但并没有完全单线程

Node的实际上单线程是对用户(使用 Node.js 进行上层开发的程序员,而不是开发 Node.js 的人员)而言的。Node.js 在底层对多个 I/O 操作是借助多线程实现异步非阻塞的,具体来说,Node.js 总是存在一个主线程,用来管理调度 I/O 线程并进行运算,而其他的线程都是 I/O 线程。I/O 线程在主线程的调度下与系统内核进行交互完成完成 I/O 操作并把数据返回给主线程,而主线程对 I/O 线程的调度就完全是上述异步非阻塞的(至于 I/O 线程是异步还是同步、阻塞还是非阻塞,已经不重要了,因为它不影响主线程的效率,只要它能按时返回预期的数据就行)。我们平时所说的 Node.js 是单线程的,就是指 Node.js 的主线程。I/O 线程完全是对用户屏蔽的,所以用户根本无需关心。
这也解释了为什么我们要避免书写计算密集型或者阻塞的代码,一旦主线程被阻塞,那整个应用就是真的都被阻塞了。

理想的异步非阻塞模型(以文件读取为例)

完整来说,一个最高效且理想的文件读取异步非阻塞模型应该是这样的:应用层发起调用后系统内核立即返回(还没有文件内容数据),应用层继续做其他无关的事情,在系统内核从磁盘读取完数据之后主动通知应用层任务已完成,应用层此时接收系统内核返回的数据,然后继续做其他相关或不相关的事情。
可以看到,在这个模型中,没有无谓的挂起、休眠与等待,也没有盲目无知的问询与检查,应用层做到不等候片刻的最大化利用自身的资源,系统内核也十分「善解人意」的在完成任务后主动通知应用层来接收任务成果。
所以一个理想的非阻塞异步IO应当是: 应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务,只需在IO完成后通过信号或者回调将数据传递给应用程序.

再来谈谈Node异步的实现

Node的异步IO

本文只对事件循环、观察者、请求对象、执行回调等概念做出理解,并没有深入理解代码实现。

首先,node的异步I/O不等于非阻塞I/O。非阻塞I/O调用后会虽然也是立即返回,但是应用层会不断的重复I/O操作去轮询系统是否完成数据读取,让CPU处理状态判断,对CPU造成资源的浪费。

然而在异步I/O里,基于多子线程的方式去解决了非阻塞I/O的问题,应用层(主线程)发起I/O请求后,就不再过问情况了。然后让子线程来完成数据获取,当读写完成后通知主线程。


这里,主线程就是nodejs所谓的单线程,也就是用户javascript代码运行的线程。

IO线程是由Libuv(Linux下由libeio具体实现;window下则由IOCP具体实现)管理的线程池控制的,本质上是多线程。即采用了线程池与阻塞IO模拟了异步IO。

事件循环
当子线程完成并通知主线程后,主线程在什么时机去调用呢?这时候就得说到事件循环。事件循环类似于while(true)的一个程序,在每次循环的时候,都会去检查一下是否有事件待处理,如果有就取出执行,否则继续下一次循环。

观察者
那主线程是去哪里检查呢,这里就要引入观察者这个概念,每次事件循环中有一个或者多个观察者,按照优先级依次进行询问和处理。
所以,事件循环是一个典型的生产/消费模型,异步I/O、网络请求等是事件的生产者,这些事件被传递到对应的观察者那里,等待事件循环取出并处理。

请求对象
当调用异步I/O时,会创建一个请求对象,该对象包含了传入的参数和当前方法上下文,以及我们最关注的回调函数。创建完成后,将其推入线程池中等待执行。至此,主线程的调用立即返回,继续执行当前任务的后续操作。

执行回调
当线程池中有可用线程池时,会对请求对象进行调用,执行完毕后,会将执行结果记录在对象中,归还线程,并且告诉观察者自己已经ready,然后观察者会把它放入队列中。
当主线程来询问观察者时,就可以取出请求对象,并且执行上面的回调函数。至此,整个异步I/O的流程就结束了。


那么事件循环是什么时候进行的呢?还是说事件循环一直在后台线程中跑着?
我们来逐步深入

(function ()
{
    console.log('this is the start');
    setTimeout(function cb()
    {
        console.log('Callback 1: this is a msg from call back');
    }); // has a default time value of 0
    console.log('this is the end');
})();
// node main.js
this is the start
this is the end
Callback 1: this is a msg from call back

再加一个setTimeout玩玩

(function ()
{
    console.log('this is the start');
    setTimeout(function cb ()
    {
        console.log('Callback 1: this is a msg from call back');
    }); // has a default time value of 0
    console.log('this is just a message');
    setTimeout(function cb1 ()
    {
        console.log('Callback 2: this is a msg from call back');
    }, 0);
    console.log('this is the end');
})();
// node main.js
this is the start
this is just a message
this is the end
Callback 1: this is a msg from call back
Callback 2: this is a msg from call back

虽然setTimeout是0ms后立即调用,但是'this is the end'也是先于两个setTimeout执行
再来看下面这个

(function ()
{
    console.log('this is the start');
    setTimeout(function cb()
    {
        console.log('Callback 1: this is a msg from call back');
    }); // has a default time value of 0
    console.log('this is just a message');
    setTimeout(function cb1()
    {
        console.log('Callback 2: this is a msg from call back');
    }, 0);
    for(let i = 0; i < 100000; i++)// 100000可大可小
    {
        console.log('this is the end');
    }
})();


由于for循环打印太多,这里采用vscode优化输出,可以明显看出不管for循环跑了几秒,两个setTimeout 0ms异步函数永远是在for循环执行完成后输出。这也说明了,如果真正想做到异步IO操作,主线程应该尽量避免大量的耗时计算或调用阻塞函数。
由于setTimeout是异步api自然符合nodejs的异步处理流程,并且setTimeout会到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。 每一轮Event Loop时,都会将“任务队列”中需要执行的任务,一次执行完。
setTimeout和setInterval都是把任务添加到“任务队列”的尾部。因此,它们实际上要等到当前脚本的所有同步任务执行完,然后再等到本次Event Loop的“任务队列”的所有任务执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。

setTimeout唯一能保证的就是会在指定时间之后执行里面的回调函数。

这里的setTimeout只是异步api,异步IO的api又是另外一回事了,等待继续深入学习

下面是整理的思维导图很多知识细节还有待深入学习、理解。右键新标签页打开查看大图。

参考文章:

  1. https://zhuanlan.zhihu.com/p/22707398
  2. https://zhuanlan.zhihu.com/p/61807318
  3. https://zhuanlan.zhihu.com/p/93289115
  4. https://blog.csdn.net/yezhenxu1992/article/details/51731237

声明:一个萌新|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 深入浅出Nodejs学习笔记——第三章 异步IO


耳不闻人之非,目不视人之短,口不言人之过。