作者: Xie Wang
1、JavaScript 是多线程吗?
Google 一下关于JavaScript 是否是单线程的热搜关键词还是挺多的,但是,我⾸首先需要跟大家澄清这种说法本⾝身就存在错误!JavaScript 和线程之间本来就是两个不同方面的东西:线程是程序执行的最小单元,是进程中的⼀一个实体;而JavaScript 只是⼀一种解释型的编程语言。所以换种⽐比较恰当说法应该是:JavaScript 运行采用单线程或者多线程模式执行。
作为解释型语言的JavaScript 本⾝身并不能决定单线程或者多线程,实际上就JavaScript 语言来说,它核心也没提到过任何线程的概念,它关心的只是语法规则,其他的跟他没半毛钱关系。决定JavaScript 是否多线程执⾏行是Runtime。JavaScript 的Runtime 有如chrome 中V8、Node.js 等。
下面我先从一个比较典型的JavaScript 代码来看清单线程问题。
var time = new Date();
var newTime = time;
setTimeout(function(){
var outTime = new Date();
console.log(outTime - time);
},1000);
while(newTime-time <4000){
newTime = new Date();
}
我们在编程中常常会用到这个Timer,它让我们误以为JavaScript 是多线程的,因为感官上我们觉得这个定时任务与其他代码一同并发执行。其实并非如此:
执行这段代码,你会发现setTimeout 的内部回调并没有在1 秒后顺利执行(事实上,若无阻塞,也并非在1 秒后执行,通常会大于1 秒),而是拖延到来4 秒以后,因为它被while 这段代码阻塞,只有等到while 执行完毕,setTimeout 的回调任务才能进入执行栈中得到执行。这就可以看出JavaScript 语言本身并没有开启一个新的线程去执行定时器中的任务,当然它也没有这个能力,它严格按照单线程模式顺序执行。
2、JavaScript 为什么采用单线程?
为什么会单线程执行,这个历史原因现在无从考究,到底是反复论证还是拍脑袋敲定的结果我也不清楚,但有些理由还是很有道理的:设计GUI 框架,多线程的方式抢占资源是很容易造成死锁的,特别是在操作DOM 上,如果两个线程同时执行,一个执行删除元素,一个执行添加元素,页面就会造成混乱,作为和人直接接触的图形界面,如果出现这种现象,基本上就算是反常识了:明明我添加一行文字,为什么平白无故消失了?你可能会说,添加更过的管理机制来管理这些线程,来防止死锁和管理执行顺序等啊。是的,
不过这可能会大大增加复杂度,博弈之下单线程更合适。网上有关于GUI 为什么很难使用多线程的论证:Multithreaded toolkits: A failed dream ,有兴趣的话可以去看看。
3、JavaScript 如何“多线程”执行?
既然JavaScript 单线程执行,为何我再次提多线程执行?很显然这是标题党才会干的事(心机婊…)。没有多线程没关系,JavaScript 有个更厉害的武器:异步回调!它能够实现JavaScript 的非阻塞运行。
这里我们要区分一下“异步”和“多线程”。很多时候我们会把两者混淆在一起,觉得他们是同一个概念,他们确实都能够实现非阻塞线程,提高运行效率。但实际上两者还是存在着很大的区别的。多线程是属于进程下的可以并发执行的代码,是操作系统的一种逻辑功能,它需要CPU 的直接运行和调度;异步则执行前获得CPU 的一个执行指令,完后返回一个中断或者完成消息给CPU,中间过程则完全不需要CPU 的干预,例如I/O、HttpRequest 等比较费时的硬件网络操作都利用这个异步逻辑。这个就像我们单核单线程的大脑(当然我不敢排除多核大脑的大神),在认真看代码找bug 的时候不能同时思考今天晚饭吃什么,却可以一边抖腿。
总结一点就是:JavaScript 的Runtime 将不耗时的计算指令在指定的一个主线程中不间断执行,其它耗时的任务,Runtime 会利用回调交给浏览器,由浏览器另行开辟线程来执行,最后将结果集返回给主线程就可以了。
4、JavaScript 如何实现异步回调?
这个就要说到一个非常重要的概念:Event Loop。
Event Loop 是一种运行机制,它能够实现异步、防止代码阻塞,JavaScript 正是运用这一机制。Philip Roberts 在他的演讲(《Help, I'm stuck in an event-loop》)中做来一个比较生动的解释,我们这里引用他的一张图作外参考,同时还有一个动态网页模拟 可供参考理解。
1)、这里有三个概念:Heap、Stack、Queue
Heap:表示一个已从内存中申请但未使用的比较大的未被组织的区域;
Stack:表示由函数调用形成的Frames 栈,先进后出;
Queue:消息队列,耗时的任务都会放在这里等待,先进先出,当栈为空时插入栈中;
2)、解释一下上图所表示的JavaScript 运行过程:
Runtime 同步地一条一条执行Stack 中的JavaScript 代码,在Stack 中的代码将会优先执行,先进后出;由于Runtime 的宿主(浏览器)包含渲染引擎、数据存储、网络通信、
JavaScript 引擎等组件,当JavaScript 需要调用这些组件的时候就会遇到阻塞问题,于是就将这些任务放入Queue 中等待Stack 清空后进入Stack。
3)、那如何将这些任务放入Queue 呢?
浏览器定义里很多Web API 来包装这些组件,当JavaScript 请求这些API 时(如window.setTimeout, XMLHttpRequest,EventTarget.addEventListener() ),浏览器则开
启一个新的线程来处理这些请求,这些线程将任务交给操作系统、或者网络服务等去执行。很显然是这里开启新线程和JavaScript 单线程并不冲突,这些线程隶属于浏览器进程,与JavaScript 线程并行运行。
当这些APIs 请求完成时,回调函数会放入一个Queue 中。Callback queue 等待Stack,当Stack 为空时,Queue 依次按先进先出的原则逐个放入stack 中执行,这就是一次完整的Loop。
4)、哪些API 需要异步执行?
浏览器会有专门的线程来管理Loop,其中有一个问题就是什么样的API 会是异步,需要插入队列等待。我咨询了Philip Roberts,他给我的回复是:凡是带有回调函数作为参数的API 都是异步的,因为他需要有异步处理的回调插入队列中等待执行,而像window.console.log(‘foo’),这样的API 没有带回调函数,所以不需要异步处理,也就不是异步的API。
Mail:
5)、这里我们顺便来看下“回调”和“异步”的区别:异步常常伴随的是回调的使用,回调也是JavaScript 中比较重要的概念, 我们本能上觉得回调即异步,这当然是错的;回调和异步不等价,没有回调,JavaScript 仍然可以是异步,有回调,也可能是同步的!以promise为证,promise 本质是为了解决回调嵌套的问题,优化代码结构,若它本身没有继承并实例化一个异步API,它并不会异步执行。我们可以对比下面两段代码:
code1
console.log('==================start================')
function asyncFunction() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('Async Hello world');
}, 3000);
});
}
asyncFunction().then(function (value) {
console.log(value);
}).catch(function (error) {
console.log(error);
});
console.log('==================no block================');
code2
console.log('==================start================');
function asyncFunction() {
return new Promise(function (resolve, reject) {
var time = new Date();
var newTime = time;
while(newTime-time <3000){
newTime = new Date();
}
resolve('Async Hello world');
});
}
asyncFunction().then(function (value) {
console.log(value);
}).catch(function (error) {
console.log(error);
});
console.log('==================block================');
code1 将回调放入setTimeout 中,由浏览器的定时线程维护,所以后面的代码不会受到阻塞;而code2 没有用到任何webAPI,后面代码被阻塞需要等待。
综上,JavaScript 实现非阻塞运行的方式是异步,异步则使用到浏览器的线程来执行比较耗时的API,而JavaScript 本身仍是单线程。
5、WebWorker 是个什么?
H5 中提出了一个新的东西:WebWorker。它实际上是后台开启一个新的线程,主线程仍然单线运行,他本质上没有改变JavaScript 单线程运行的特性。每个JavaScript 执行线程都需要一个Runtime,WebWorker 在多个Runtime 之间进行通信。例如它在两个Frame 之间传递数据,解决跨域问题。
SubFrame:
MainFrame:
主Frame 不断给子Frame 传递数据,子Frame 接受数据后返回成功信息给主Frame,主Frame 获得成功消息后停止消息发送。
6、Node.js 为什么使用JavaScript?
在Node.js 的运行环境中,JavaScript 也是单线程的。而它不同与浏览器调用webAPIs 实现异步,而是调用node 底层的libuv 提供的异步I/O。
既然Node.js 不存在GUI 框架所遇到那样特别繁琐的资源抢占问题,作为服务器语言,为什么Node.js 不使用多线程,而是延用JavaScript 的单线程执行的特性,采用事件驱动模型呢?这之间固然又是一番孰优孰劣的博弈。这里可以参考一篇关Event 和Thread 之间比较的博客 。
个人觉得,代码的发展同时伴随着硬件的发展,过去在几百K 的内存上为了能够抢占资源,各种线程之间的竞争必不可少,现在的硬件内存越来越快大、传输速度也越来越快,保持一个主线程的有序执行、其它任务分布开来,效果上也是显得游刃有余。这就是为什么Node.js 官网上会宣称:Node.js uses an event-driven, non-blocking I/O model that makes itlightweight and efficient。
7、总结
这篇文章主要是说JavaScript 异步的核心原理,关于现在日趋流行的async、Promise、Generator 等都只不过对它的包装,解决“回调地狱”等问题,让代码看起来更加优雅,编
写起来更爽而已。万变不离其宗,搞懂了底层的原理,再来看这些框架,就有会有拨开迷雾见明月的感觉。