JavaScript并发模型和事件循环
1、简介
JS有一个基于“事件循环”的并发模型。这个模型和其他语言(如C和Java)的模型不太一样。
下图描述的是一个理论模型,现代JS引擎在此基础上实现和进行了很多优化:
2、模型详细介绍
1)相关概念
(1)栈Stack:如下例,在调用g之前,以及在g返回之后,栈都为空。
function f(b) { var a = 12; return a + b + 35; } function g(x) { var m = 4; return f(m * x); } g(21);
(2)堆:这里不介绍。
(3)队列:待处理消息的列表,每条消息都关联一个回调函数。
JavaScript是单线程的(多进程/多线程往往有更大的内存开销、上下文切换开销和数据竞争问题等),这意味着需要有一个队列保存异步执行的代码。
I、入队操作。例如,某个按钮被按下时,它的事件处理器代码被添加到队列;接收到某个Ajax响应时,回调函数的代码被添加到队列;对于定时器,当指定时间过去后将其回调函数的代码添加到队列。
II、出队操作。队列中没有任何代码是立刻执行的,但一旦进程空闲(此时栈为空)则尽快执行。进程每次从队列中取出一条消息并调用其回调函数(异步操作的结果通过回调函数获得)。这使得栈变为非空。当栈再次变为空时,表示该消息处理完毕。
(4)事件循环。得名于它通常的实现方式:
// 所谓事件循环,就像代码从一个循环中不断取出而运行一样 while (queue.waitForMessage()) { queue.processNextMessage(); } // waitForMessage():当前没有消息时执行同步等待
维基百科的定义:event loop/message dispatcher/message loop/run loop/...是一个程序结构,用于等待和分发(dispatch)事件或消息。
node.js依赖于libev提供的事件循环。
2)模型特点:
(1)完整运行(Run-to-completion):每条消息处理完成后,再处理其他消息。
进入一个函数后,只有在它完整运行后才会“切换”到其他代码,从而无需担心函数操作的数据被意外修改。
不足之处在于,如果一条消息处理时间过长,则Web应用程序无法响应用户的交互操作,浏览器将提示"a script is taking too long to run"。一个好的做法是缩短消息处理过程,并尽可能把一条消息“切分”为多条。
例子:
setTimeout(function cb() { console.log('5 seconds timeout'); }, 5000); setTimeout(function cb() { console.log('9 seconds timeout'); }, 9000); setTimeout(function cb() { console.log('7 seconds timeout'); }, 7000); setTimeout(function cb() { console.log('3 seconds timeout'); }, 3000); for(var start = +new Date; +new Date - start <= 10000; ) {} // “模拟”睡眠
运行大约10s后,输出:
3 seconds timeout 5 seconds timeout 7 seconds timeout 9 seconds timeout
(2)从不阻塞(也有例外,如alert或同步XHR,但最好避免使用它们):I/O的处理一般借助于事件和回调函数,因此当应用程序在等待一个IndexedDB查询或一个XHR请求返回时,它仍可以处理其他事情。
参考资料:
Concurrency model and Event Loop
《JavaScript异步编程》
《JavaScript高级程序设计》
不断学习中。。。