浅谈 Node.js 中的异步编程原理和实践

本文深入探讨了Node.js中的异步编程原理,包括非阻塞IO、事件队列、异步事件循环,以及多种异步编程方式如回调函数、Promise、async/await等。文章指出,Node.js的单线程特性避免了多线程问题,利用非阻塞IO和事件循环实现高性能。同时,文章提醒开发者注意IO饿死问题,并提供了最佳实践建议。
摘要由CSDN通过智能技术生成

由于 JavaScript 是单线程运行的,如果单线程的所有程序都是同步执行的,那么一旦某段程序调用堵塞,整个线程就挂起了。所以 JavaScript 天生是异步的。

Node.js 使用的主要编程语言是 JavaScript,采用异步编程,其主要特点如下:

  1. 单线程相比多线程而已,最大的劣势就是无法充分使用利用多核 CPU。
  2. 但是单线程也避免了多线程中的存在的一些问题:线程的创建和上下文切换开销大以及多线程经常面临锁,状态同步问题。
  3. 而采用异步 I/O 编程,远离线程被外部调用所阻塞,可以充分使用单核CPU。
  4. 为了弥补单线程无法使用多核 CPU 的缺点,Node 提供了子工作进程的方式去高效使用 CPU。
  5. 最后我们在部署的服务的时候,同一台机器可以部署多个实例,可以充分利用 CPU。

Node.js 中异步原理

阻塞 IO/非阻塞 IO

什么叫 IO 呢?一般是指除了 CPU 之外的外部设备的任务都叫 I/O 操作。最常见的 I/O 操作类型就是文件操作和 TCP/UDP 网络操作

操作系统对计算机进行了抽象,将所有输入输出设备定义成文件,内核在进行 IO 操作时,通过文件描述符进行管理,应用程序在进行 IO 调用时,根据文件描述符去实现 IO 数据的读取,非阻塞 IO 和阻塞 IO 的区别在于,阻塞 IO 需要完成整个文件的读取过程,而非阻塞 IO 可以不带数据直接返回,然后再根据文件描述符区轮询查询返回数据。而 Node 正式利用非阻塞 IO 实现异步编程的。

异步 IO 原理

异步 IO 是指应用层以异步的方式去读取非阻塞 IO 的方式,只有非阻塞 IO 才能执行异步操作。Node 底层采用线程池的原理管理异步 IO,所以我们通常所的 单线程是指 Node 中 JavaScript 的执行是单线程的,但 Node 本身是多线程的。Node.js 中异步 IO 是通过事件循环的方式实现的,异步 IO 事件主要来源于网络请求和文件 IO。事件循环主要由以下几个部分实现:

  • 事件循环:Node 启动进程后,便会创建一个类似 while 的循环,每次循环我们称为一个 tick,在循环的过程中,每次都要查看是否有事件需要处理,如果有,则取出处理,如果没有事件需要处理则直接退出。
  • 观察者:每个事件循环中,会有一个多个观察者存在,事件循环的过程就是不断询问观察者有没有需要处理的事件的过程。
  • 请求对象:请求对象是异步 IO 的中间产物,所有状态都保存在这个请求对象中,包括送入线程池以及 IO 操作完毕的回调处理,告诉观察者。
  • 线程池:多个线程池按照一定的算法并发执行请求对象,执行完请求对象通知 IOCP 调用完成,通知观察者,放入观察者列表

node异步原理图解

而异步 IO 的事件调用模型在不同的操作系统上实现不一样,Linux 系统中是 epoll, 在 BSD 系统(MacOS)中是 kqueue, 在 Solaris 系统中是 event ports, 在 Windows 系统中是 IOCP(Input Output Completion Port)。但是 Node.js 使用 libuv 做统一封装,兼容所有平台的异步事件逻辑。

非 IO 的异步 API

Node 中并非所有的异步都是 IO,还有一些非 IO 的异步 API,主要有 setTimeout(),setInterval(),setImmediate(),process.nextTick(),resolved 的 Promise他们的实现原理与异步 IO 类似,只是不需要 IO 线程池参与,同样存在观察者的概念,也是每次事件循环去检查有没有需要执行的观察者。

事件队列的分类

事件队列主要分为以下六类,其中前四类属于原生的 libuv 事件循环队列,后两种属于被 Node 处理的 “中间队列”,不同队列存放的数据结构也不尽相同:

  1. Time 事件:使用 setTimeout 和 setInterval 函数到了一定时间执行的回调函数,存储在红黑树的堆中。
  2. IO 事件:已经就绪准备执行的 IO 事件。
  3. setImmediate 事件:使用 setImmediate 添加的回调函数事件,存储在链表中。
  4. Close Handlers 事件:任意 close 事件的触发的回调。
  5. Next Ticks 事件:使用 process.nextTick 添加的回调函数,存储在数组中。
  6. 其他 Microtasks 任务事件:例如 resolved 的 Promise 回调,Promise.then。
事件队列的优先级以及执行顺序

正如你下面看到的这个图,Node 从检查定时器队列中有没有到期的定时器回调函数开始,随后在每个步骤中检查其他所有队列,并维护一个引用计数器来记录需要被处理的项目总数。在处理完 close 事件队列之后,如果在所有队列中都没有需要被处理的项目,那么事件循环将会退出。事件循环中的每个队列的处理可以看作是事件循环的一个阶段。

事件队列执行顺序

对于使用红色标红的中间队列来说,有趣的在于,只要一个阶段完成之后,事件循环会去检查那两个中间队列是否有可执行的项目。如果有,那么事件循环将会立刻开始处理这两个中间队列的项目直到队列为空。当它们为空,事件循环才会继续下一个阶段的处理。

运行案列1:

setTimeout(function() {
   
    console.log('setTimeout');
}, 0);
setImmediate(function() {
   
    console.log('setImmediate');
});

上面代码的执行顺序是不可确定的,setTimeout 函数如果设置超时时间为 0,Node 会自动转换 1,受系统性能的影响,如果系统性能差,第一次 tick 循环就检查到 setTimeout 时间到了,那么会优先执行第一个,但是如果系统性能好的话,第二次或者第三次 tick 循环才会时间到,那么则先执行第二个。

运行案列2:

const fs = require('fs');

fs.readFile(__filename, () => {
   
    setTimeout(() => {
   
        console.log('timeout');
    }, 0);
    setImmediate(() => {
   
        console.log('immediate');
    });
});

上述循环是确定的,一定先打印 immediate,后打印 timeout。因为 fs.readFile 回调函数执行完以后,setImmediate 和 setTimeout 分别被添加到对应的队列,接下来是轮到 setImmediate 队列执行,所以先执行 setImmediate 对应的回调。

运行案列3:

//setImmediate
setImmediate(function(){
   
     console.log(1);
     setImmediate(function(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值