异步I/O

为什么要异步I/O

异步I/O、事件驱动、单线程构成了Node的基调,Node可以作为服务器端去处理客户端带来的大量并发请求,也能作为客户端向网络中的各个应用进行并发请求。

使用异步I/O的两个原因:

用户体验

1、同步会在数据请求阶段阻塞用户与UI界面的交互;

2、前端获取资源的速度:多个资源的请求在同步情况下的耗时是M+N,而异步是Max(M, N),随着应用复杂性的增加,异步的优势将越来越大。

资源分配

假设业务场景中有一组互不相关的任务需要完成,现行的主流方法有以下两种:单线程串行依次执行 和 多线程并行完成。

  • 多线程

多线程的代价在于创建线程和执行期线程上下文切换的开销较大。而且,多线程变成经常面临锁,状态同步等问题。但是多线程的优势是在多核CPU上能够有效提升CPU的利用率。

  • 单线程

在计算机资源中,通常I/O与CPU计算之间是可以并行进行的,但是同步的编程模型会导致的问题是,I/O的进行会让后续任务等待,造成资源的浪费。

Node的解决方案

利用单线程,远离多线程死锁、状态同步等问题,利用异步I/O,让单线程远离阻塞,以更好地使用CPU。为了弥补单线程无法利用多核CPU的缺点,Node提供了类似前端浏览器中Web Workers的子进程,该子进程可以通过工作进程高效地利用CPU和I/O。

异步I/O的提出是期望I/O的调用不再阻塞后续运算,将原有等待I/O完成的这段时间分配给其余需要的业务去执行。

异步I/O实现现状

非阻塞I/O

阻塞I/O的一个特点是调用之后一定要等到系统内核层面完成所有操作后,调用才结束。

非阻塞I/O跟阻塞I/O的差别为调用之后会立即返回。不会花时间去等待,充分利用了CPU。

但是非阻塞I/O也存在一些问题,由于完成的I/O并没有完成,立即返回的并不是业务层期望的数据,而仅仅是当前调用的状态,为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成,这种重复调用判断操作是否完成的技术叫轮询。

阻塞I/O造成CPU等待浪费,非阻塞I/O带来的麻烦却是需要轮询去确认是否完全完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。轮询技术演进,就是以减少 I/O状态判断的CPU损耗 为目的进行的。

epoll: 是Linux下效率最高的I/O时间通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒,它是真实利用了事件通知,执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。但是休眠期间CPU几乎是闲置的,对于当前线程而言利用率不够。

轮询技术满足了非阻塞I/O确保获取完整数据的需求,但是应用程序仍然需要花费时间等待I/O完全返回,等待期间,CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生,结论是它不够好。

理想下的异步I/O

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

现实的异步I/O

多线程方式的异步I/O设想:通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递,这就轻松实现了异步I/O。

libeio是一个异步I/O的库,是采用线程池与阻塞I/O模拟异步I/O,最初Node在*nix平台下采用了libeio实现异步I/O,在Node v0.9.3中自行实现线程池来完成异步I/O。

而在windows平台下的异步I/O方案则是IOCP:调用异步方法,等待I/O完成之后的通知,执行回调,用户无须考虑轮询,但是它的内部其实仍然是线程池原理,不同之处在于这些线程池由系统内核接受管理。

由于windows平台和*nix平台的差异,Node提供了libuv作为抽象封装层,使得所有平台兼容性的判断都由这一层来完成,并保证上层的Node与下层的自定义线程池及IOCP之间各自独立。Node会在编译期间判断平台条件,选择性编译unix目录或win目录下的源文件到目标程序中。

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

Node的异步I/O

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

异步I:O的流程.png

事件循环

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

观察者

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

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

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

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

请求对象

从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它就叫请求对象。

从JavaScript调用Node的核心模块,核心模块调用c++内建模块,内建模块通过libuv(封装层,兼容两个平台)进行系统调用。在这个调用过程中,就会封装一个请求对象,把JavaScript层传入的参数和当前方法都封装在这个对象上,其中回调函数也被设置在这个对象上。

请求对象封装完成之后,就将这个请求对象推入线程池中等待执行,至此JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束,当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到JavaScript线程的后续执行。

执行回调

异步调用的第二部分就是执行回调,当I/O操作完成之后,会将获取的结果存在请求对象的result属性上,并发出操作完成通知,并将线程规还线程池。

每次事件循环时会检查 I/O 线程池中是否存在已经完成的 I/O 操作,如果有就将请求对象加入到I/O观察者队列当中(事件队列),之后当作事件处理。

事件循环时查看到有事件需要处理,就会从I/O观察者中取到可用的请求对象,从中取出回调函数和请求结果并调用执行。

非I/O的异步API

Node中还存在一些与I/O无关的异步API,它们分别是setTimeout()、setInterval()、setImmediate()和process.nextTick()。

定时器

setTimeout()、setInterval()与浏览器中的API一致,调用setTimeout()或者setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中,每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将立即执行。

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观察者,setTimeout()属于I/O观察者,在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

事件驱动与高性能服务器

事件驱动的实质:即通过主循环加事件触发的方式来运行程序。

几种典型的服务器模型

同步式:对于同步式的服务,一次只能处理一个请求,并且其余请求都处于等待状态。

每进程/每请求:为每一个请求启动一个进程,这样可以处理多个请求,但是它不具备扩展性,因为系统资源只有那么多。

每线程/每请求:为每一个请求启动一个线程来处理,尽管线程比进程要轻量,但是由于每个线程都要占用一定内存,当大并发请求到来时,内存将会很快用光,导致服务器缓慢,每线程/每请求的扩展性比每进程/每请求的方式要好,但对于大型站点而言依然不够。

Node通过事件驱动的方式处理请求,无须为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销。

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

推荐阅读更多精彩内容