背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效。

已读完书籍《架构简洁之道》。

当前阅读周书籍《深入浅出的Node.js》

异步I/O

为什么要异步I/O

1、从用户体验方面看

JavaScript在执行的时候UI渲染和响应是处于停滞状态的。如果脚本的执行时间超过100毫秒,用户就会感到页面卡顿,以为网页停止响应。

采用异步请求,在下载资源期间,JavaScript和UI的执行都不会处于等待状态,可以继续响应用户的交互行为,给用户一个鲜活的页面。

前端获取资源的速度取决于后端的响应速度。依旧可以采用异步方式享受到并发的优势,第一个资源的获取并不会阻塞第二个资源,也即第二个资源的请求并不依赖第一个资源的结束。

2、从资源分配方面看

假设业务场景中有一组互不相关的任务需要完成,现行的主流方法有以下两种:

  • 单线程串行依次执行。
  • 多线程并行完成。

多线程在多核 CPU 上能够有效提升 CPU 的利用率,但也经常面临锁、状态同步等问题。

单线程顺序执行任务的方式易于表达,但是串行执行的缺点在于性能,任意一个略慢的任务都会导致后续执行代码被阻塞。

Node 在两者之间给出了它的方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步 I/O,让单线程远离阻塞,以更好地使用 CPU。

阅读周·深入浅出的Node.js | 异步I/O、事件驱动和单线程,Node基调三元素了解一下_非阻塞

异步I/O实现现状

操作系统对异步I/O实现的支持状况如下。

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

操作系统内核对于I/O只有两种方式:阻塞与非阻塞。

在调用阻塞I/O时,应用程序需要等待I/O完成才返回结果。

非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回。非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,此时的性能提升是明显的。

阅读周·深入浅出的Node.js | 异步I/O、事件驱动和单线程,Node基调三元素了解一下_回调函数_02

2、理想的非阻塞异步I/O

非阻塞I/O也存在一些问题。由于完整的I/O并没有完成,立即返回的并不是业务层期望的数据,而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。

所以期望的完美的异步I/O应该是应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可。

阅读周·深入浅出的Node.js | 异步I/O、事件驱动和单线程,Node基调三元素了解一下_事件循环_03


3、现实的异步I/O

现实要达成理想异步I/O的目标,并非不可能。

通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询的技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递,这就轻松实现了异步I/O(尽管它是模拟的)。

阅读周·深入浅出的Node.js | 异步I/O、事件驱动和单线程,Node基调三元素了解一下_非阻塞_04

Node的异步I/O

事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素。

1、事件循环

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程被称为Tick。

每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。

2、观察者

每个事件循环中有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

3、请求对象

对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫做请求对象。

如下代码,fs.open()的作用是根据指定路径和参数去打开一个文件,从而得到一个文件描述符,这是后续所有I/O操作的初始操作。

fs.open = function(path, flags, mode, callback) {
  // ...
  binding.open(pathModule._makeLong(path),
                stringToFlags(flags),
                mode,
                callback);
};

4、执行回调

组装好请求对象、送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。

I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,以此达到调用JavaScript中传入的回调函数的目的。

阅读周·深入浅出的Node.js | 异步I/O、事件驱动和单线程,Node基调三元素了解一下_非阻塞_05

非I/O的异步API

Node中还存在一些与I/O无关的异步API,它们分别是setTimeout()、setInterval()、setImmediate()和process.nextTick()。

1、定时器

setTimeout()和setInterval()与浏览器中的API是一致的,分别用于单次和多次定时执行任务。

2、process.nextTick()

process.nextTick()方法的操作相对较为轻量,也更高效。process.nextTick()的回调函数保存在一个数组中,在每轮循环中会将数组中的回调函数全部执行完。

3、setImmediate()

setImmediate()方法与process.nextTick()方法十分类似,都是将回调函数延迟执行。setImmediate()的结果则是保存在链表中,在每轮循环中执行链表中的一个回调函数。

事件驱动与高性能服务器

事件驱动的实质,即通过主循环加事件触发的方式来运行程序。

Node通过事件驱动的方式处理请求,无须为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价很低。这使得服务器能够有条不紊地处理请求,即使在大量连接的情况下,也不受线程上下文切换开销的影响,这是Node高性能的一个原因。

Node是一套高性能的平台,可以利用它构建与Nginx相同的功能,也可以处理各种具体业务,而且与背后的网络保持异步畅通。

总结

我们来总结一下本篇的主要内容:

  • 异步I/O、事件驱动和单线程,它们构成Node的基调。
  • 事件循环是异步实现的核心,它与浏览器中的执行模型基本保持了一致。
  • Node依靠构建了一套完善的高性能异步I/O框架,打破了JavaScript在服务器端止步不前的局面。

作者介绍非职业「传道授业解惑」的开发者叶一一。《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。