likes
comments
collection
share

浅析NodeJS非阻塞异步I/O

作者站长头像
站长
· 阅读数 21

前言:

NodeJS是一个基于Chrome V8引擎的javascript运行时环境

Node使用: 单线程 非阻塞I/O 事件驱动的方式实现了高并发请求,libuv提供了异步编程的能力

1 几种常见的I/O

目前现有的几种I/O模型

  1. 阻塞I/O
  2. 多线程阻塞I/O
  3. 非阻塞I/O
  4. I/O多路复用:select/poll/epoll
  5. 异步I/O

阻塞I/O

阻塞I/O是read/write函数的默认执行机制,会在读写操作执行时将进程设置为阻塞态,I/O完成之后,系统中断将其更改为就绪状态,等待时间片分配,继续执行下一次I/O或者函数。

缺点: 无法并发执行I/O操作,无法同时在I/O操作执行的同时请求CPU计算,如果一个请求读取状态发生阻塞,那么其他请求无法处理

优点: 简单易懂,代码实现简单 适用于小型的命令行工具,简单程序

多线程阻塞I/O

有很多思路,这里学习一个 使用多线程

初始化一个线程池,利用信号量的 wait 原语进入阻塞状态。等到有 I/O 操作需求时,通过信号量signal将线程唤醒并执行相关的 I/O 操作

缺点:多线程非阻塞 I/O 有个弊端,就是当连接数达到很大的一个程度时,线程切换也是一笔不小的开销。

非阻塞I/O

非阻塞 I/O 是一种机制,允许用户在调用 I/O 读写函数后,立即返回,如果缓冲区不可读或不可写,直接返回 -1。

返回后就可以同时执行其他cpu的计算了。

缺点:

  1. 如果 while 循环轮询等待执行的操作,会造成不必要的 CPU 运算的浪费,因为此时 I/O 操作未完成,read 函数拿不到结果;
  2. 如果使用 sleep/usleep 的方式强行让进程睡眠一段时间,又回造成 I/O 操作的返回不及时。

I/O多路复用

就是一个进程内同时执行多个I/O操作, 分为 select, poll, epoll(macos 上的替代品是 kqueue) 这几个阶段

select:select 作用是可以批量监听 fd,当传入的 fd_set 中任何一个 fd 的缓冲区进入可读/可写状态时,解除阻塞,并通过 FD_ISSET 来循环定位到具体的 fd

select带来的问题:fd_set(集合)允许的最大数量是1024 ; 每次执行select函数都会存在fd_set的拷贝,轮训存在性能开销

解决上述问题需要poll出场:

poll:poll函数将接受的fd集合更改为数组,就没有了1024的限制, 但是性能开销的问题还存在,由 epoll解决

epoll:epoll有三个阶段 epoll_create;epoll_ctl ; epoll_waite

epoll_create在fd绑定阶段控制不需要传入重复fd集合

epoll_ctl 将传入的fd在内核态维护一颗红黑树,当I/O操作完成时,通过红黑树以 O(LogN) 的方式定位到 fd,避免轮询;

epoll_waite:监听任意的fd发生变化之后解除阻塞

缺点:epoll 目前只支持 pipe, 网络等操作产生的 fd,暂不支持文件系统产生的 fd。

异步I/O

无论是阻塞 I/O 还是 非阻塞 I/O 还是 I/O 多路复用,都是同步 I/O。都需要用户等待 I/O操作完成,并接收返回的内容。而操作系统本身也提供了异步 I/O 的方案,对应到不同的操作系统:

  1. Linux
    1. aio,目前比较被诟病,比较大缺陷是只支持 Direct I/O(文件操作)
    2. io_uring, Linux Kernel 在 5.1 版本加入的新东西,被认为是 Linux 异步 I/O 的新归宿
  1. windows
    1. iocp,作为 libuv 在 windows 之上的异步处理方案。(笔者对 windows 研究不多,不多做介绍了。)

前面写了很多.这里来参考一个大佬的总结链接

  • 阻塞I/O: 在发起I/O操作之后会一直阻塞着进程,不执行其他操作,直到得到响应或者超时为止;
  • 非阻塞I/O:发起I/O操作不等得到响应或者超时就立即返回,让进程继续执行其他操作,但是要通过轮询方式不断地去check数据是否已准备好
  • 多路复用I/O:又分为select、pool、epool。最大优点就是单个进程就可以同时处理多个网络连接的IO。 基本原理就是select/poll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。 而epool通过callback回调通知机制.减少内存开销,不因并发量大而降低效率,linux下最高效率的I/O事件机制。
  • 同步I/O:发起I/O操作之后会阻塞进程直到得到响应或者超时。前三者阻塞I/O,非阻塞I/O,多路复用I/O都属于同步I/O。 注意非阻塞I/O在数据从内核拷贝到用户进程时,进程仍然是阻塞的,所以还是属于同步I/O。
  • 异步I/O:直接返回继续执行下一条语句,当I/O操作完成或数据返回时,以事件的形式通知执行IO操作的进程

上面介绍了几种主流的I/O,下面开始将今天的重点 非阻塞I/O

2 为什么选择非阻塞异步I/O

单线程

前面提到 node是基于Chrome V8引擎的javascript运行时

注意两个关键点:Chrome javascript; 也就是说node的运行环境是浏览器 语言是javascript

1 javascript语言本身就是单线程的,优点是不会像多线程(java)语言在编程是出现线程同步,线程锁的问题,避免了上下文切换带来的性能开销问题。

2 在浏览器环境下只能使用单线程,否则多个线程对同时对DOM操作会乱套。

node定位

再来看nodejs的定位:提供一种简单安全的方法在 JavaScript 中构建高性能和可扩展的网络应用程序

传统的服务器语言大都是多线程。阻塞式I/O,在用户建立连接时,每个连接都是一个线程,十万个用户建立连接,就有十万个线程,

node采用 非阻塞异步I/O,最大的优势就是性能强,同样的服务器性能如果使用node可以比传统服务器语言多容纳100倍的用户,(I/O操作越多,node的优势越明显),擅长I/O密集型任务,可以使用在一些I/O密集型的高并发场景中。所以node的定位也选择非阻塞异步I/O也更加适合

这里有个非常有意思的解释:【Node.js】如何理解非阻塞I/O(详解)

3 非阻塞异步I/O的实现

实现异步非阻塞I/O需要做两件事情:1.实现非阻塞。2 实现异步

首先分析一下非阻塞异步I/O的过程:在node执行了读取数据库的代码之后,将立即转为执行后面的代码,把读取数据返回的结果的处理代码放到回调函数中。当某个I/O执行完毕的时候,node用事件的形式通知执行I/O操作的线程,然后线程执行这个事件对应回调函数(中的处理函数)

对于一个网络IO 涉及到的两个系统为:

1 调用这个系统的进程或者线程;

2 系统内核

出现I/O操作时 两个阶段:

1 系统内核把数据准备好

2 将数据从系统内核中拷贝到用户进程中。

这里再来区分一下:

阻塞I/O和非阻塞I/O区别在于:在I/O操作的完成或数据的返回前是等待还是返回(可以理解成一直等还是分时间段等) 同步I/O和异步I/O区别在于 :在I/O操作的完成或数据的返回前会不会将进程阻塞(或者说是主动查询还是被动等待通知


有了上面的基础知识,我们继续学习node中是怎么实现非阻塞异步I/O的

实现非阻塞:

通过事件循环来实现。

事件循环的原理单独放一篇来讲,这里简述一下,每个node进程有一个主线程在执行代码, 同时维护一个事件队列,在遇到网络请求或者异步操作的时候,此操作会先放到事件队列中排队(不会马上执行),同时主线程的代码也会继续执行, 当主线程的代码执行完成之后,通过事件循环机制,检查队列中 是否有要处理的事件,然后从队列头中取出事件,并分配线程处理事件,全部执行完毕之后,事件队列通知主线程,执行回调,把线程归还给线程池。

实现异步

实现异步的基础也是事件循环:

nodejs中的事件循环分为以下六个阶段,每种回到都会在对应的阶段:这里I/O的异步是在poll阶段,poll阶段组要处理的就是IO输入和输出是否就绪。

浅析NodeJS非阻塞异步I/O

这里分为网络请求异步io和文件的异步io

网络请求的异步IO是在事件循环的基础上poll阶段完成的,前面我们提到poll函数将接受的fd集合更改为数组,是poll的特点,

其实select poll和eppll都有一个共同的特点:在io轮询阶段,会有一个timeOut时间。也就是轮询结束时间,

在io操作时,如果读取文件成功,会直接退出当前轮询,如果一直没有成功,那么在timeOUt时间到达后退出当前轮询。

但是存在一个问题 异步I/O只能存在linux系统中,且无法利用系统缓存,windows对此的解决方案是使用多线程 IOCP 这个系统API(其内部还是用线程池完成的)。

实现异步IO结论:

1 node调用异步方法

2 node核心模块创建对应的文件I/O观察者对象

3 根据不同平台(Linux或者window)在poll阶段,使用了操作系统内核中的io机制中timeout方式,实现异步

4 回调通知

文件的异步IO

调用的是libuv库的线程池

libuv 使用 epoll 来构建 event-loop 的主体,其中:

  1. socket, pipe 等能通过 epoll 方式监听的 fd 类型,通过 epoll_wait 的方式进行监听;
  2. 文件处理 / DNS 解析 / 解压、压缩等操作,使用工作线程的进行处理,将请求和结果通过两个队列建立联系,由一个 pipe 与主线程进行通信, epoll 监听该 fd 的方式来确定读取队列的时机

过程拆解:

1 创建请求I/O请求对象(包含回调函数)

2 timeout结束或者获取到数据之后推入线程池,调用返回

3回调通知

参考链接:

Node.js 异步非阻塞 I/O 机制剖析

Node.js理论实践之《异步非阻塞IO与事件循环》

阻塞对比非阻塞一览

nodejs是如何实现非阻塞异步io的?

转载自:https://juejin.cn/post/7231835495557578810
评论
请登录