异步I/O

为什么要异步 I/O

用户体验

只有后端能够快速响应资源,才能让前端的体验变好

资源分配

利用单线程,远离多线程死锁、状态同步等问题;利用异步 I/O,让单线程远离阻塞,以更好地使用 CPU

异步 I/O 实现现状

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

轮询技术满足了非阻塞 I/O 确保获取完整数据的需求,但是对于应用程序而言,它仍然只能算是一种同步,因为应用程序仍然需要等待 I/O 完全返回,依旧花费了很多时间等待。等待期间,CPU 要么用于遍历文件描述符的状态,要么用于休眠等待时间发生

理想的非阻塞异步 I/O

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

现实的异步 I/O

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


基于libuv的架构示意图.png

我们时常提到 Node 是单线程的,这里的单线程仅仅只是 JavaScript 执行在单线程中罢了。在 Node 中,无论是 *nix 还是 Windows 平台,内部完成 I/O 任务的另有线程池

Node 的异步 I/O

事件循环——Node 自身的执行模型

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

Tick流程图.png

观察者——在每个 Tick 的过程中,判断是否有事件需要处理

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

事件可能来自用户的点击或者加载某些文件时产生,而产生的事件都有对应的观察者。在 Node 中,事件主要来源于网络请求、文件 I/O 等,这些事件对应的观察者有文件 I/O 观察者、网络 I/O 观察者等。观察者将事件进行了分类。

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

在 Windows 下,这个循环基于 IOCP 创建,而在 *nix 下则基于多线程创建

请求对象——从 JavaScript 发起调用到内核执行完 I/O 操作的过渡过程中的中间产物

从 JavaScript 调用 Node 的核心模块,核心模块调用 C++ 内建模块,内建模块通过 libuv 进行系统调用,这里的 libuv 作为封装层,有两个平台的实现,实质上是调用了 uv_fs_open() 方法。在 uv_fs_open() 的调用过程中,我们创建了一个 FSReqWrap 请求对象。从 JavaScript 层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关注的回调函数则被设置在这个对象的 oncomplete_sym 属性上:req_wrap->object->Set(oncomplete_sym, callback);

对象包装完毕后,在 Windows 下,则调用 QueueUserWorkItem() 方法将这个 FSReqWrap 对象推入线程池中等待执行。

至此,JavaScript 调用立即返回, 由 JavaScript 层面发出的异步调用的第一阶段就此结束。JavaScript 线程可以继续执行当前任务的后续操作。当前的 I/O 操作在线程池中等待执行。不管它是否阻塞 I/O,都不会影响到 JavaScript 线程的后续执行,如此就达到了异步的目的。

请求对象是异步 I/O 过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及 I/O 操作完毕后的回调处理

执行回调

线程池中的 I/O 操作调用完毕之后,会将获取的结果储存在 req->result 属性上,然后调用 PostQueuedCompletionStatus() 通知 IOCP,告知当前对象操作已经完毕:PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))

PostQueuedCompletionStatus() 方法的作用是向 IOCP 提交执行状态,并将线程归还线程池。

在这个过程中,我们其实还动用了事件循环的 I/O 观察者。在每次 Tick 的执行中,它会调用 IOCP 相关的 FetQueuedCompletionStatus() 方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到 I/O 观察者的队列中,然后将其当做事件处理。

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

整个异步IO的流程.png

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

在 Node 中,除了 JavaScript 是单线程外,Node 自身其实是多线程的,只是 I/O 线程使用的 CPU 较少。另一个需要重视的观点则是,除了用户代码无法并行执行外,所有的 I/O(磁盘 I/O 和网络 I/O 等)则是可以并行起来的。

非 I/O 的异步 API

定时器

setTimeout() 和 setInterval() 与浏览器中的 API 是一致的,分别用于单次和多次定时执行任务。它们的实现原理和异步 I/O 比较类似,只是不需要 I/O 线程池的参与。定时器的问题在于,它并非精确的(容忍范围内)。尽管事件循环十分快,但是如果某一次循环占用的事件较长,那么下次循环时,它也许已经超时很久了。


setTimeout()的行为.png

process.nextTick()

采用定时器需要动用红黑树,创建定时器对象和迭代等操作,而 setTimeout(fn, 0) 的方式较为浪费性能。实际上 process.nextTick() 的方法的操作相对较为轻量。

每次调用 process.nextTick() 方法,只会将回调函数放入队列中,在下一轮 Tick 时取出执行。定时器中采用红黑树的操作时间复杂度为 O(lg(n)),nextTick() 的时间复杂度为 O(1)。相较之下, process.nextTick() 更高效。

setImmediate()

setImmediate() 方法与 process.nextTick() 方法十分类似,都是将回调函数延迟执行

区别是,process.nextTick() 中的回调函数执行的优先级要高于 setImmediate()。这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick() 属于 idle 观察者,setImmediate() 属于 check 观察者。在每一次轮循环检查中,idle 观察者先于 I/O 观察者,I/O 观察者先于 check 观察者

在具体实现上,process.nextTick() 的回调函数保持在一个数组中,setImmediate() 的结果则是保存在链表中。在行为上,process.nextTick() 在每轮循环中会将数组中的回调函数全部执行完,而 setImmediate() 在每轮循环中执行链表中的一个回调函数。

// 加入两个nextTick()de 回调函数
process.nextTick(function () {
 console.log('nextTick延迟执行1');
});
process.nextTick(function () {
 console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
 console.log('setImmediate延迟执行1');
 // 进入下次循环
 process.nextTick(function () {
 console.log('强势插入');
 });
});
setImmediate(function () {
 console.log('setImmediate延迟执行2');
});
console.log('正常执行');
// 其执行结果如下:
//// 正常执行
//// nextTick延迟执行1
//// nextTick延迟执行2
//// setImmediate延迟执行1
//// 强势插入
//// setImmediate延迟执行2

从执行结果上可以看出,当第一个 setImmediate() 的回调函数执行后,并没有立即执行第二个,而是进入了下一轮循环,再次按 process.nextTick() 优先、setImmediate() 次后的顺序执行。之所以这样设计,是为了保证每轮循环能够较快地执行结束,防止 CPU 占用过多而阻塞后续 I/O 调用的情况。

事件驱动与高性能服务器

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

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

推荐阅读更多精彩内容

  • 1.为什么要使用异步I/O 1.1 用户体验 浏览器中的Javascripts是在单线程上执行的,并且和UI渲染公...
    maikuraki阅读 535评论 0 0
  • I/O简介 1.I/O操作:内核在进行文件I/O操作时,通过文件描述符(fd:一个整数—应用程序和内核之间的凭证)...
    励志摆脱懒癌的少女酱阅读 1,681评论 0 1
  • Node的异步I/O 我们为什么需要异步I/O? 用户体验服务器端如果基于同步执行的,随着应用复杂性的增加,响应的...
    俗三疯阅读 490评论 0 0
  • 单线程编程会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程也因为编程中的死锁、状态同步等问题让开发人员头痛。...
    exialym阅读 431评论 0 1
  • 异步IO实现现状 I/O的阻塞与非阻塞:IO对于操作系统内核而言,只有阻塞与非阻塞两种方式。阻塞模式的I/O会造成...
    fangPeng__阅读 1,798评论 0 0