异步编程的核心
程序中现在运行的部分和将来运行的部分之间的关系。
异步概念
同步的Ajax
var data=ajax(url);
console.log(data)
任何时候都不应该使用同步的方式,因为它会锁定浏览器的UI线程
将来执行的块
任何时候只要把一段代码包装成一个函数,并指定它在响应某个事件时执行,你就创建了一个将来执行的块,这就是异步。
事件循环
JS引擎运行在宿主环境中,比如浏览器,比如OS(NODE),他们提供了一种机制来处理程序中多个块的执行,且执行每块时调用JS引擎,这种机制被称为事件循环。
原因
JS引擎本身只是一个按需执行JS任意代码片段的环境,而事件的调度是交给了包含它的环境进行,比如浏览器,Node。
EventLoop
//仅仅是简化的代码。
var eventLoop=[];
var event;
//永远执行
while(1){
//一次tick
if(eventLoop.length>0){
//拿到队列中的下一个事件
event=eventLoop.shift();
//现在执行下一个事件
try{
event();
}catch(err){
reportError(err);
}
}
}
可以看到,我们的代码块实际上是暂存到了一个队列中,然后FIFO执行它。
原理
循环的每一轮我们称为TICK
对每个TICK来说,如果队列中有等待事件,那么就会从队列中摘下
一个事件并执行。即你的回调函数被执行一个。
setTimeout和事件循环。
setTimeout并没有把你的回调函数挂在事件循环队列中。它做的是设定一个定时器。
当定时器到时后,环境*会把你的回调函数放在事件循环中* ,然后未来某个时刻执行这个回调!
所以setTimeout才会不准确! 精度不高!!! 它要根据事件队列的状态来定!!!
异步和并行
度进程和线程独立运行,并可能同时运行在不同的处理器或不同的计算机上,但是多个线程能够共享单个进程的内存。
异步:但是事件循环把自身的工作分成一个个任务顺序执行,
不允许对共享内存的并行访问和修改~
共存
通过分立线程中彼此合作的事件循环。
其实并行和异步是可以同时存在的,
因为并行线程的交替执行和异步事件的交替调度,粒度是完全不同的!
多线程
尽管在JS中是没有多线程的。 我们还是看看如果并行运行下面这段程序会发生什么事情?
function later(){
answer=answer*2;
console.log("Meaning of life:",answer);
}
尽管later()的所有内容被看作一个单独的事件循环队列项,
也就是说我们在异步的思想来看,它是一个整体的单元。
但是如果以多线程的思想来看,这里的每一语句,还必须细分。
比如:
answer=answer*2;
过程
需要先加载answer的值,然后把2放到某处
并执行乘法,取值,然后存到answer。
(操作系统是不断取指执行的) 所以说多线程要细分到一个原子
操作(不会被线程调度机制打断的操作)。
不妨做个假设
var a=20;
function foo(){
a=a+1;
//实际为
//把a的值加载到X(内存地址)
//把1保存到Y(内存地址)
//执行X+Y,结果存在X
//把X的值保存在a
}
function bar(){
a=a*2;
//实际为
//把a的值加载到X
//把2的值存到Y
//执行X乘以Y,存到X
//把X的值保存在a
}
foo();
bar();
如果是有两个线程在运行,foo在bar之前或者bar在foo之前运行,不光如此,它们两个函数中的原子操作也可能交替执行,它会得到不确定的结果。所以是非常复杂的。
总的来说
由于JS单线程的原因,所以foo()和bar(),也就是函数块的代码具有原子性,也就是说它们执行的时候不会被其他事情或任务打断。同步执行没什么好说,它是现在运行,结果是确定的。
对于异步,将来执行的,运行结果则不确定。
并发
浏览器端可能会触发很多事件,这些事件被触发然后会执行若干个回调函数,这些任务同时执行就出现了并发,不管组成他们的单个运算是否并行执行。我们可以把并发看作“任务级” 与运算级并行(不同于处理器上的线程)不同。
处理方式:
可以用事件循环来处理并发,把很多事件的回调函数存入队列。
竞态(race condition)
同一段代码有两个可能的输出,这种函数顺序的不确定性就是竞态.
非交互和交互
如果进程间没有相互影响的话,不确定性是可以接受的。
它的顺序不影响各自的结果,但如果他们是相互有依赖关系的。
比如
var res=[];
function response(data){
//..
}
//
ajax(url1,response);
ajax(url2,response);
这里的顺序是有依赖关系的,对res的结果有影响,执行的顺序有影响!
我们可以简单的协调:
function response(data){
if(data.url===url1){
res[0]=data;
}
else {
res[1]=data;
}
}
这就避免了竞态。
协作
并发协作。 不再是通过共享作用域中的值进行交互。
取得一个运行时间过长的任务,将其分割成多个步骤或多批任务来处理 。
使得其他并发的任务有机会将自己的运算插入到事件循环队列中交替执行。
比如:
function respon(data){
var chunk=data.splice(0,1000);
//添加到res组
res=res.concat();
}
把数据放在一个最多1000条项的块中,这样就确保了任务运行时间短。 事件循环队列的交替运行会提高性能。但我们并没有协调这些任务。所以结果的顺序仍然不可预测!
任务和事件循环
任务队列: 挂在事件循环队列的每个TICK之后的一个队列。
事件循环每个TICK中,可能出现的异步动作不会导致一个完整
的新事件添加到事件循环队列中,而会在当前TICK的任务队列末尾
添加一个项目。
任务处理是在当前事件循环TICK结尾处。