node 异步 I/O

这篇文章主要讲 nodejs 中的异步 IO,关于同步、异步、阻塞、非阻塞 请移步这里

事件循环 和 消息队列

我们常说“JavaScript是单线程的”。

所谓单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。不妨叫它主线程。

但是实际上还存在其他的线程。例如:处理AJAX请求的线程、定时器线程、读写文件的线程等等。这些线程可能存在于JS引擎之内,也可能存在于JS引擎之外,在此我们不做区分。不妨叫它们工作线程。

async_pic.png

node 执行过程

node_event.png

处理并执行完 js 代码,main函数继续往下调用libuv的事件循环入口uv_run(),node进程进入事件循环。 uv_run() 的 while 循环做的就是一件事,判断 default_loop_struct 是否有存活的 io 观察者 或 定时器。

事件循环

事件循环是指主线程重复从消息队列中取消息、执行的过程

事件循环对应上图 3 号标注的部分。用代码表示大概是这样的:

        while(true) {
            var message = queue.get();
            execute(message);
        }
event_loop.png

如上图,每一次执行一次循环体的过程称为 Tick。

事件循环的阶段:

   ┌───────────────────────┐
┌─>│        timers         │ 执行定时器(setTimeout/setInterval)注册的回调函数,也是进入事
│  └──────────┬────────────┘ 件循环第一个阶段。
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │ I/O 事件相关联的回调或者报错会在这里执行
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │ 内部使用,不讨论
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │ 最重要的一个阶段,I/O 观察者观察到线程池
│  │         poll          │<─────┤  connections, │ 里有任务已经完成,就会在这里执行回调。
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │ 专门用来执行 setImmediate() 的回调
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │ 一个连接或 handle 突然被关闭,close 事件会被发送到这里执行回调
   └───────────────────────┘

如上图,共有六个阶段(官方称为 phase)。特别要说明的是 poll 阶段,在这个阶段,如果暂时没有事件到来,主线程便会阻塞在这里,等待事件发生。当然它不会一直等下去:

  • 它首先会判断后面的 Check Phase 以及 Close Phase 是否还有等待处理的回调. 如果有, 则不等待, 直接进入下一个 Phase.
  • 如果没有其他回调等待执行, 它会给 epoll 这样的方法设置一个 timeout. 可以猜一下, 这个 timeout 设置为多少合适呢? 答案就是 Timer Phase 中最近要执行的回调启动时间到现在的差值, 假设这个差值是 delta. 因为 Poll Phase 后面没有等待执行的回调了. 所以这里最多等待 delta 时长, 如果期间有事件唤醒了消息循环, 那么就继续下一个 Phase 的工作; 如果期间什么都没发生, 那么到了 timeout 后, 消息循环依然要进入后面的Phase, 让下一个迭代的 Timer Phase 也能够得到执行.

来看一下流程:

phases.png

到这里你一定发现少了一些问题:process.nextTick() 和 Promise 都是异步的,它们对应以上哪个阶段呢?往下看

任务队列

1、运行主线程(函数调用栈)中的同步任务
2、主线程(函数调用栈)执行到任务源时,通知相应的webAPIs进行相应的执行异步任务,将任务源指定的异步任务放入任务队列中
3、主线程(函数调用栈)中的任务执行完毕后,然后执行所有的微任务,再执行宏任务,找到一个任务队列执行完毕,再执行所有的微任务
4、不断执行第三步

任务队列也叫消息队列。主要分两类任务:宏任务(macro-task)、微任务(micro-task)

宏任务:setTimeout setInterval setImmediate I/O

微任务:process.nextTick Promise 的回调

在上面的图中,各个 phase 完成了宏任务对应的事件。微任务的执行时机在每一次进入下一个阶段之前,process.nextTick 优先级大于 Promise 的回调。

FAQ

setTimeout 和 setImmediate 的比较
setImmediate(() => console.log(2))
setTimeout(() => console.log(1))

这段代码的结果实际上是不确定的。可是,为什么?按照流程图,应该是 timer 先于 check 阶段,所以应该是 setTimeout 先执行,可是为什么结果不是这样呢?首先我们要知道:

setTimeout(fn) ==> setTimeout(fn, 0) ==> setTimeout(fn, 1)

上面三个效果是一样的!前两个好理解,给定的默认值是0。其实在 node 源码中,最低为 1 ms,官方文档如下:

When delay is larger than 2147483647 or less than 1, the delay will be set to 1.

所以当进入 timer 阶段时,1ms 可能超时也可能没有,这个影响因素有很多。如果还没超时,则进入下一个 phase,依次往下,所以先输出 2 。如果已经超时,则先输出 1。

但是!如果它们在 I/O 事件回调中,那么输出顺序是固定了的,如下

require('fs').readFile('path.txt', () => {
 setImmediate(() => console.log(2))
 setTimeout(() => console.log(1))
});
// 输出: 2 1

如果不知道为什么,答案就在循环图中。

(完)

Reference

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 单线程编程会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程也因为编程中的死锁、状态同步等问题让开发人员头痛。...
    exialym阅读 427评论 0 1
  • 让I/O与CPU计算并行 Node 在*nix平台,通过线程池实现(主线程和I/O线程),在windows下使用I...
    wmtcore阅读 349评论 0 0
  • 参考:IOCP原理Philip Roberts: Help, I’m stuck in an event-loop...
    mary_s阅读 2,235评论 1 8
  • 一、JavaScript单线程模型 JavaScript是单线程的,JavaScript只在一个线程上运行,但是浏...
    Brolly阅读 1,132评论 4 6
  • 如果你是一枚硬币 一面花朵一面你的雕塑 在旅途中跟随 在我的左口袋或右口袋跳跃 亲爱的 我的手心便都是你 午后的阳...
    冬日皑皑阅读 244评论 0 2