异步I/O、事件驱动、单线程

nodejs的特点总共有以下几点

  1. 异步I/O(非阻塞I/O)
  2. 事件驱动
  3. 单线程
  4. 擅长I/O密集型,不擅长CPU密集型
  5. 高并发

下面是一道很经典的面试题,描述了node的整体运行机制,相信很多人都碰到了。这道题背后的原理就是nodejs代码执行顺序

    setTimeout(function() {
console.log('4');
},0) setImmediate(function() {
console.log('5');
}) let s = new Promise(function(resolve, reject) {
console.log('2');
resolve(true)
console.log('7')
}) s.then(function() {
console.log('3');
}) process.nextTick(function() {
console.log('6')
}) console.log('1');
// 我电脑的输出结果是 2、7、1、6、3、4、5

1. nodejs代码执行顺序(事件循环机制)

nodejs的运行机制: nodejs主线程主要起一个任务调度的作用。nodejs用一个主线程处理所有的请求, 将I/O操作交由底下的线程池处理;在所有主线程任务执行完成后,主线程处理事件队列。 所以在同步初始化代码执行完成后,nodejs会基于事件队列不停的做事件循环。事实上,nodejs运行环境 = 主线程(单线程,包括事件队列) + 线程池(工作线程池,执行其他工作-多线程)

  • node 的初始化

    • 初始化 node 环境。
    • 执行输入代码。
    • 执行 process.nextTick 回调。
    • 执行 microtasks。(Promise.then)
  • 进入事件循环
    • 进入 timers 阶段 (定时器阶段:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。)

      • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入pending IO callbacks阶段。(对某些系统操作(如 TCP 错误类型)执行回调)
      • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入 idle,prepare 阶段:
      • 仅系统内部使用。
    • 进入 poll 阶段(检索新的 I/O 事件;执行与 I/O 相关的回调,除了定时器和关闭的回调函数,其余都在这里)
      • 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。

        • 第一种情况:

          • 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
          • 检查是否有 process.nextTick 回调,如果有,全部执行。
          • 检查是否有 microtaks,如果有,全部执行。
          • 退出该阶段。
        • 第二种情况:
          • 如果没有可用回调,执行下一步;
          • 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
          • 如果不存在尚未完成的回调,退出poll阶段。
    • 进入 check 阶段。(setImmediate() 回调函数在这里执行)
      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 check 阶段
    • 进入 closing 阶段。(检测关闭的回调函数,例如 xx.on('close'))
      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 closing 阶段
        • 检查是否有活跃的 handles(定时器、IO等事件句柄)。

          • 如果有,继续下一轮循环。
          • 如果没有,结束事件循环,退出程序。

: 在主线程执行完和事件循环总共7个阶段,每一个阶段执行完都会调用一遍process.nextTick回调,一遍microtaks(promise);

2. setImmediate和process.nextTick和setTimeout

  • setImmediate(): 事件循环poll阶段执行完后执行setImmediate;
  • process.nextTick():主线程和事件循环每一阶段完成后都会调用;
  • setTimeout(): 最少经过n毫秒后执行的脚本,受到前一次事件循环时间影响,实际执行时间为>=n毫秒
  • ** setTimeout和setImmediate执行顺序问题**
    • 如果运行的是不属于 I/O 周期(即主模块)的以下脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束;
    • 如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用;I/O场景推荐使用setsetImmediate,因为setsetImmediate始终而且是立即执行

3. 对上题的理解

主线程中,console.logpromise的new方法在初始化主线程中执行,他们俩个的输出时间按照先上后下的顺序输出,他们两个执行完后会立即执行主线程的process.nextTick,然后执行promise.then方法,然后是进入事件队列中执行setTimeoutsetImmediate。因为setTimeout的
'最少经过n毫秒后执行的脚本'特性,导致无法确定setTimeoutsetImmediate的执行先后顺序,但如果是在回调函数中,则必然setImmediate先执行,因为事件循环的阶段中,setImmediate紧挨着回调函数之后执行,而setTimeout则在下次事件循环中执行。

4. 单线程和多线程

  • 多线程: 服务器为每个客户端请求分配一个线程,使用同步 I/O,系统通过线程切换来弥补同步 I/O 调用的时间开销。比如 Apache 就是这种策略,由于 I/O 一般都是耗时操作,因此这种策略很难实现高性能,但非常简单,可以实现复杂的交互逻辑。
  • 单线程: 而事实上,大多数网站的服务器端都不会做太多的计算,它们接收到请求以后,把请求交给其它服务来处理(比如读取数据库),然后等着结果返回,最后再把结果发给客户端。因此,Node.js 针对这一事实采用了单线程模型来处理,它不会为每个接入请求分配一个线程,而是用一个主线程处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。

5. 异步I/O

  • IO操作: IO操作就是以流的形式,进行的操作,比如网络请求,文件读取写入。IO操作也就是input和output的操作。

  • 阻塞IO: 在调用阻塞O时,应用程序需要等待IO完成才能返回结果。 阻塞IO的特点:调用之后一定要等到系统内核层面完成所有操作之后,调用才结束。 阻塞O造成CUP等待IO,浪费等待时间,CPU的处理能力不能得到充分利用。

  • 非阻塞IO: 为了提高性能,内核提供了非阻塞IO,非阻塞IO跟阻塞IO的差别是调用之后会立即返回。阻塞IO完成整个获取数据的过程,而非阻塞IO则不带数据直接返回,要获取数据,还要通过描述符再次读取。非阻塞IO返回之前,node主线程可以用来处理其他事物,此时性能提升非常明显。

  • 为什么node擅长I/O密集型,不擅长CPU密集型:因为node的I/O处理中主线程只负责转发,实际操作在其他线程及线程队列里完成,所以性能相对较高; 而CPU密集则要求node的主线程处理,这时候其余请求只能等待

  • 我的理解: node的异步I/O分为两个阶段,第一个阶段是主线程调用线程池里的工作线程执行异步操作,主线程取回对应的描述符,存储下来,工作线程执行相关操作取回数据后存储下来,这一部分在主线程接收到请求后立即完成;第二个阶段在事件队列里完成,根据描述符去工作线程里去获取数据,以提升性能.

6. 高并发

以下是对nodejs高并发的理解,nodejs的高并发体现在处理I/O的性能上,而不是CPU密集上,摘录自官网文档

让我们思考这样一种情况:每个对 Web 服务器的请求需要 50 毫秒完成,而那 50 毫秒中的 45 毫秒是可以异步执行的数据库 I/O。选择 非阻塞 异步操作可以释放每个请求的 45 毫秒来处理其它请求。仅仅是选择使用 非阻塞 方法而不是 阻塞 方法,就是容量上的重大区别。

7. 总结

Node 有两种类型的线程:一个事件循环线程和 k 个工作线程。 事件循环负责 JavaScript 回调和非阻塞 I/O,工作线程执行与 C++ 代码对应的、完成异步请求的任务,包括阻塞 I/O 和 CPU 密集型工作。 这两种类型的线程一次都只能处理一个活动。 如果任意一个回调或任务需要很长时间,则运行它的线程将被 阻塞。 如果你的应用程序发起阻塞的回调或任务,在好的情况下这可能只会导致吞吐量下降(客户端/秒),而在最坏情况下可能会导致完全拒绝服务。要编写高吞吐量、防 DoS 攻击的 web 服务,您必须确保不管在良性或恶意输入的情况下,您的事件循环线程和您的工作线程都不会阻塞。

通常意义上,I/O密集型活动,如网络I/O、文件I/O,DNS操作等通常建议放在对外提供网络服务的端口所在的服务内,剩下的诸如大内容的crypto,zlib,fs同步操作、子进程,JSON处理、计算等尽量另起node服务或者其他语言服务去进行,因为这些操作会影响到node的主线程的性能和安全性。

参考

  1. Node.js 事件循环机制
  2. nodejs笔记之:事件驱动,线程池,非阻塞,异常处理等
  3. 官网文档
  4. Node.js 事件循环,定时器和 process.nextTick()
  5. nodejs 事件循环
  6. 不要阻塞你的事件循环(或是工作线程池

题外话

事实上,对于nodejs的相关理解更多的收获在于这里,nodejs官网指南的中文文档,以前有点粗心了

异步I/O、事件驱动、单线程的更多相关文章

  1. QT 信号槽 异步事件驱动 单线程 多并发

    利用好Qt 模块的异步信号槽,单线程同样可是实现很强悍的的并发能力.应付正常的功能是足够的. 需要注意的是:该模式本质上为 单线程 事件驱动异步模式,所以需要做的事优化你的业务代码构架以应付性能与并发 ...

  2. Python之路-python(Queue队列、进程、Gevent协程、Select\Poll\Epoll异步IO与事件驱动)

    一.进程: 1.语法 2.进程间通讯 3.进程池 二.Gevent协程 三.Select\Poll\Epoll异步IO与事件驱动 一.进程: 1.语法 简单的启动线程语法 def run(name): ...

  3. 浅谈JS异步轮询和单线程机制

    单线程特点执行异步操作 js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务就会排队形成一个任务队列排队等候执行.一般而已,相对耗时的操作是要通过异步 ...

  4. Select\Poll\Epoll异步IO与事件驱动

    事件驱动与异步IO 事件驱动编程是一种编程规范,这里程序的执行流由外部事件来规定.它的特点是包含一个事件循环,但外部事件发生时使用回调机制来触发响应的处理.另外两种常见的编程规范是(单线程)同步以及多 ...

  5. Python 协程/异步IO/Select\Poll\Epoll异步IO与事件驱动

    1 Gevent 协程 协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到 ...

  6. python Select\Poll\Epoll异步IO与事件驱动

    参考:http://www.cnblogs.com/alex3714/articles/5248247.html 写服务器处理模型的程序时通常采用的模型: (1)每收到一个请求,创建一个新的进程,来处 ...

  7. day10 消息队列,多进程和多线程以及协程,异步IO,事件驱动等

    回顾一下线程和进程 线程与进程的区别 守护线程: 队列: 两种方式: 先进先出  # 后入先出   #卖水果,后来的来的是新的 生产者消费者模型: 生产包子, 吃包子 事件 event: 红绿灯模型 ...

  8. node.js的异步I/O、事件驱动、单线程

    nodejs的特点总共有以下几点 异步I/O(非阻塞I/O) 事件驱动 单线程 擅长I/O密集型,不擅长CPU密集型 高并发 下面是一道很经典的面试题,描述了node的整体运行机制,相信很多人都碰到了 ...

  9. js的单线程与异步

    一. js 是单线程和异步 1. js 是单线程的,js 的宿主环境(浏览器)是多线程的,实现异步. 2.js是单线程语言,浏览器值分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务, ...

随机推荐

  1. leetcode解题报告(26):Add Binary

    描述 Given two binary strings, return their sum (also a binary string). For example, a = "11" ...

  2. LOJ2269. 「SDOI2017」切树游戏 [FWT,动态DP]

    LOJ 思路 显然是要DP的.设\(dp_{u,i}\)表示\(u\)子树内一个包含\(u\)的连通块异或出\(i\)的方案数,发现转移可以用FWT优化,写成生成函数就是这样的: \[ dp_{u}= ...

  3. php-fpm nginx 超时参数设置

    php-fpm:request_terminate_timeout = 30php.ini:max_execution_time = 30 request_terminate_timeout 适用于, ...

  4. Codeforces 1264C/1265E Beautiful Mirrors with queries (概率期望、DP)

    题目链接 http://codeforces.com/contest/1264/problem/C 题解 吐槽:为什么我赛后看cf的题就经常1h内做出Div.1 C, 一打cf就动不动AB题不会啊-- ...

  5. 并发用户 VS TPS

    TPS模式(吞吐量模式)是一种更好的方式衡量服务端系统的能力. 基本概念: 并发用户数:简称VU ,指的是现实系统中操作业务的用户,在性能测试工具中,一般称为虚拟用户数(Virutal User),注 ...

  6. vue项目用户登录状态管理,vuex+localStorage实现

    安装vuex cnpm install vuex --save-dev

  7. T-MAX组--项目冲刺(第四天)

    THE FOURTH DAY 项目相关 作业相关 具体描述 所属班级 2019秋福大软件工程实践Z班 作业要求 团队作业第五次-项目冲刺 作业正文 T-MAX组--项目冲刺(第四天) 团队名称 T-M ...

  8. PostgreSQL定时自动备份

    PostgreSQL定时自动备份 简介 PostgreSQL数据库中未提供数据库的定时备份功能,所以需要结合备份和定时job功能来共同实现. 这里我选取了2种定时job方式,crontab是Linux ...

  9. qt 单例程序

    1.http://qt.nokia.com的网站把QtSingleApplication 的源代码qtsingleapplication-2.6_1-opensource.zip 下载下来,然后解压缩 ...

  10. openssl从内存中读取私钥进行签名

    麻痹的找了好久,真恶心! #include <stdio.h> #include <stdlib.h> #ifdef WIN32 #include <windows.h& ...