nodejs系统学习(二)——I/O操作

1,718 阅读6分钟

前言

本文主要是希望对node的知识点做一个整理,之前学习的比较散,难免会有疏漏,希望可以通过文章进行一个系统的梳理和学习,同时也会将一些面试题整理进来加强学习。关于node的基本介绍就不多赘述,直接从知识点开始,本文是系列的第二篇——异步I/O。

意义

对于node来说使用JavaScript语言以及选择V8引擎作为JavaScript解释器,注定了主线程只能是作为单线程存在,而作为一个服务端应用,并发是不得不面对的一件事,那么单线程实现异步IO就显得非常重要,同时也是早期Nodejs最赖以自豪的优势。

注:本质上node在处理异步I/O时还是多线程,只是主线程是单线程

单线程

解决的问题

操作系统内核对I/O操作进行处理只有两种方式,阻塞和非阻塞。

起初,大部分实现 I/O 操作的库都是阻塞型 I/O。因此在传统的多线程机制下,虽然可以处理并发事件,但是由于创建线程和执行器线程上下文切换等额外的开销,造成了很多性能上的浪费,导致这种方式的性能不高。

通过事件循环机制实现单线程实现异步I/O 的 node,使I/O密集型处理成为了node的优势。

之前说过node在处理异步I/O时也是多线程,那么早期node和传统的多线程(比如java)比如何做到这个优势的?(其实现在来说优势并不大了)

  • java一个用户一个线程,也就是进行一次请求就会开启一个线程,而node只有在需要进行阻塞操作时才会开启多线程,这样同样并发的情况下,node同时开启的线程数要小于java。
  • V8引擎由C++编写,很多底层服务都是C++处理的,本身性能并不差
  • 通过event loop机制管理事件处理(早期这种思想并不常见)

Node的异步IO

对node来说,操作系统内核对I/O操作的处理阻塞或不阻塞区别并不大,因为阻塞或不阻塞是针对操作系统的,对node来说也只是开一个I/O线程等待返回的结果,大概如下图:

调用流程

在实现调用I/O的过程中,node使用libuv作为抽象封装层,对于平台进行了兼容。在node编译期间会根据不同的条件选择调用封装好的I/O模型进行使用,如下图:

再次强调:本质上node在处理异步I/O时还是多线程,只是主线程是单线程

关于libuv开启I/O线程操作等内容可能会单开一章,本文暂不多说。

事件循环

node通过事件循环机制来管理绝大部分的异步事件,包括所有的I/O操作。
事件循环也是在主线程上完成的,在进程启动时node会创建一个无限执行的循环,每次执行循环体,都会查看是否有事件在等待或未处理,只有当所有的事件都处理完才会退出进程,如下图:

而如果一旦开始处理事件循环将会经历下面6个阶段

  • timers(定时器):
    本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • pending callbacks(待定回调):
    执行延迟到下一个循环迭代的 I/O 回调。此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。
  • idle, prepare:
    仅系统内部使用。
  • poll(轮询):
    检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • check(检测):
    setImmediate() 回调函数在这里执行。
  • close callbacks(关闭的回调函数):
    一些关闭的回调函数,如:socket.on('close', ...)。

总的来说,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

在node中,每个事件循环中会有一个或者多个观察者,这些观察者都注册了相关的事件,等待事件的完成,并调用回调函数。(例如node中的网络IO观察者、文件IO观察者等)事件循环会不断的从观察者那里取出事件并处理,最终返回回调函数。

运行流程

最后回顾一下整个异步I/O的流程如下图:

面试题

1.请介绍一下Node事件循环的流程

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们成为Tick。 每个Tick的过程就是查看是否有事件待处理。如果有就取出事件及其相关的回调函数。然后进入下一个循环,如果不再有事件处理,就退出进程。

2.在每个tick的过程中,如何判断是否有事件需要处理呢?

每个事件循环中有一个或者多个观察者,而判断是否有事件需要处理的过程就是向这些观察者询问是否有要处理的事件。 在Node中,事件主要来源于网络请求、文件的I/O等,这些事件对应的观察者有文件I/O观察者,网络I/O的观察者。 事件循环是一个典型的生产者/消费者模型。异步I/O,网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。在windows下,这个循环基于IOCP创建,在*nix下则基于多线程创建。

3.描述一下整个异步I/O的流程

见上文

总结

本文主要是对异步I/O重要内容的一些整理,也参考了非常多其他作者的文章,希望能对您有帮助。

参考文章

www.jb51.net/article/171…
nodejs.org/zh-cn/docs/…
juejin.cn/post/684490…
www.cnblogs.com/liuchuanfen…