异步编程 Async JavaScript 在 Node 面前获得前所未有的重视。本文结合 Trevor Burnham 所著 《Async JavaScript Build More Responsive Apps with Less Code(中文名: JavaScript 异步编程:设计快速响应的网络应用)》一书,梳理 JavaScript 的异步编程的方方面面。
为更好地了解异步开发的来龙去脉,我们先回顾一下 JS 服务端的历史,看到底解决了什么问题(当然是否真的解决是另外一个问题),并以此来与 Node 横向比较。
- 早期出现的 Javascript 服务端:Netscape LiveWire,它将阻塞式 I/O包含在内,使用多进程模
- 上世纪九十年代中期出现的 ASP,微软很给力,除了主流的 VBS 编程模式,还提供有基于 JScript 的。此时期到后来的 Aptana 的 Jaxer 也是没有突破旧的同步模式
- 2009 年,Node 问世了,其特点:异步、非阻塞、高并发。这里说明一下,“异步”、“非阻塞”、“高并发”三者之间的关系可以说是一层一层递进的
实现异步的关键,简单讲,在于“闭包(Closures)”。函数是 JS 为第一等公民,可以把函数作为对象调来调去,并一个函数轻易地包含另外一个函数;或者被另外一个函数包含着,也没有问题。——故所以,在 JS 里面闭包是天然支持的。从泛语言的角度讲,任何有闭包的语言都是函数式语言,区别在于写起来是否轻松,越轻松的话那么越名副其实。这样的话,JS 具备了那么多优点,如果把 JS 应用在非阻塞环境,例如 Socket 网络编程中,是否可行呢? Node 作者 Ryan Dahl 就是这样的想的,于是尝试将 V8 与 非阻塞的 C 代码结合起来,从而诞生了 Node。后来事实证明,Ryan 的方式不仅可行且表现不俗。
综合几个方面,我们不妨再思考下这些问题:
- 异步编程、事件模型、事件队列、阻塞/非阻塞、异步函数/回调函数……等等,这一堆名词有神马区别与联系?假设我们要以此进行苦逼的考试,这势必将是一堆名称解析的题型:(
- 如果我们是从前端过来的,怎么发现前端没有“大谈特谈”异步编程,这又是为何?
- 到底 JS 怎么实现异步编程?最朴素的 JS 底层机制究竟是怎样的?
- 更重要的,我的 异步 JS 应该怎么写法好?
实际上,包括 JAVA 在内的许多 API 如 JDK 都提供 NIO 非阻塞版本之接口。如果使用多线程模型编码,面临着若干问题。先着眼于两点:
- 线程是贵重的。在一定环境下,使用线程的数量是有限制的。
- 为保证数据的线程安全性,需要调用互斥量或者信号量来封装数据。这将增加我们代码的复杂性。
多线程模型下固然同样可以处理非阻塞调度,只是相比事件模型消耗的资源来得更大,尤其长连接的场景,最能体现这种不足。另外编码成本也更高。关于两者的分析,小弟在旧文已经探讨过,详见《学习NodeJS第二天:漫谈NodeJS 》。
前端没有“大谈特谈”异步编程
实际上,不论前端抑或后端,都会遭遇一个问题,就是如何优雅应对复杂事件集的范畴,这仍属于 JavaScript 有待解决的前沿领域。
历史与当前总结
从1995 年诞生的年份开始起,本属“草根”到不得了的 JavaScript 于 AJAX 革命成功之后“颠覆性”地一路走来并在茁壮地成长,除了 VB Script 正面较量外,其他的 RIA 应用还不算有真正的威胁,Flash、Sliverlight、Java Fx 你方唱罢我登台,对 JavaScript 倒也可算“小打小闹”。
不管怎么样,JavaScript 就坚守在浏览器的阵地。Google Gmail 之于 JavaScript 所倚重的力量有目共睹,于是数以百计的项目纷纷把 JavaScript 派上前端。这一热潮更是催生了 JS Runtime 之竞争态势,Apple 的 Safari/Webkit、Mozilla 的 Firefox OS、连微软的 Metro 界面开发都把 HTML5 置于与 C#、VB 平起平坐的地位。
若说语言有生涯,这便是 JavaScript 生涯中的第一个转折点。
渐渐地,JavaScript 成为一门体面的语言。这固然与原发明者出色的设计理念有关;更重要的是,拜无处不在的浏览器所赐——JavaScript 比任何语言都有资格兑现了 Java 那古老的承诺“一次编写、到处运行”。
然而,没有下一站“给力”的开发潮流,恐怕一切美好都是“虚火”,不足以让人们把视野关注在 JavaScript 身上。如果把 JavaScript 比作一个男人,他会有第二个转折点吗?
不如将 JavaScript 真正可应用于服务端编程,岂不是更好!?这种全端的开发模式不用说也是顺理成章的。
——恰好,Node 出现了。
关于 Node 本身的优点已经铺天盖地了,不想多分析,但要探讨的是,Node 之出现对于 JavaScript 的“利导”不见得也是一帆风顺的。
JavaScript 设计的初衷是为了强化 Netscape 浏览器的展现能力,仅仅是脚本之目的。不曾想,现在业已成为多媒体、多任务、多内核网络世界中一员,然而微妙的却是,JavaScript 并没有摇身一变成为支持多线程的语言(也许随着语言规范的发展也会加入),而是稳固单线程的一门语言(早已超出脚本,可称为语言了,或者界限已经模糊了)。
下面我们花大量篇幅来介绍扫服务端事件驱动开发的概念。理解这些概念是实践 JS 异步编程的关键。
关于 JS 的一些认识
在许多语言中,事件模型不属于语言级别的支援,而是由外层 API 提供。但 JS 事件一直是语言的核心;
前面已经提到,JS 对闭包天然的支持。这姑且不以晦涩的闭包概念深入原理,只是明白
引入线程的概念是为了“并行”,可以处理多个任务同时开始,同时执行,同时进行,从而整体上加快最终效率。多个线程对应多项任务,多个对多个的分工这很好理解。但我们知道, Node JS 始终是单线程程序,却怎么运行多个任务,而且效率反而高?——这怎么说?实际上,我要告诉大家三点,1)JS 的确同一时间内只会做一件事,这是所谓单线程的表现;2)那岂不是同步意思了,难道不能并行了吗?但没关系,且看;3)我们把 JS 设计为一个“圈 Loop”,一个可以永远(当然也可以手工或者强行中止的)运行下去的循环,同时这个循环结构上是个队列,允许你不断往这个队列加入新任务。如果发现处理完毕的事件,则从队列中剔除表示执行完毕;如果没处理完毕,嗯~这个循环看了看之后不做什么,继续走下去不停留。又因为是循环的缘故,尚未完成的任务又会被事件机制访问,直到执行完毕为止。如此便可以把多个任务“不落单”地处理完毕。
是不是到这里,问题就完了?不对~感觉多任务执行如何哪里,你始终还没有说清楚,对不对?
嗯,能够提出深入的发问很好。尽管我没有挖据 V8 & Linux 代码去论证,但相信凭借我的自圆自说,个中的原理是这样的:
- JS 再一次成为了接口语言,在 Node V8 里面封装了 C/C++ 底层,呈现一套以 JS 语法的 API。但实际运算仍交到 C/C++ 执行;
- 假设这些任务都需要一定的耗时来完成,也就是说,使用异步的场景是适合的;
- 使用 Node 不等于你程序跑起来就是纯粹的单线程程序,得从微观角度观察;
- 因为 JS 不允许同时多个任务,但底层的 C/C++ 可以,于是底层运行着的是多个线程,因此多个任务同时运行成为可能。
- 事件队列中既可以是 JS 闭包,也是可以是系统的调用。
虽然 JS 初始化了底层去执行任务,但 JS 并不干涉任务的过程,不关心任务怎样完成,他只需要知道最终结果,ok (触发用户成功的回调)还是不 ok(触发 err 事件)。JS 知不知道这些多个线程存在?当然知道,但决不会把它们暴露出来,而是自己“事件循环”的机制来调度。你可以把 JS 这一层面想象为一个指挥者、总的调度者,它催生了多个任务同时跑,然后经常不辞劳苦地围着任务列表(即“事件队列”)在转,一个一个挨着问,“搞定没有”?“没有吗?”,“没有,在弄呢”,“好,我不催你”,继续访问下家……周而复始。这个过程中,系统事件内部是有多线程在跑的。只是我们不晓得而已,我们看到的只是 JS 层面的机制。这固然是 Node 优雅的地方,也是其卖点所在:不用线程却能操控多任务。也许不了解的人以为这是 JS 的“魔法”所赐,但无论如何形容,我们的任务就是要摸清楚这套 JS 机制,为我所用。
若要以线程称呼 JS,那么 JS 便是单线程程序。但有没有多线程的 JS?有!更确切地说,你可以在你程序中让多线程参与进来,但 JS 基本单元就只是一条线程。未来 JS 语言规范会出现线程处理的关键字,不是没有这种可能。但现在 JS 单线程模型是简单的、朴素的、友好的。当然有许多方式为你的 JS 程序提供多线程的支持。下面我们也分别进行介绍。假设不借助其他手段,一个 JS 程序有且只有一个事件循环队列。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Gone:', end -start, 'ms'); }, 200); // alert(11) while((new Date - start) < 1000){} // 强行阻塞 // alert(33)
……未完待续……