生活中的同步异步

假设现在火车站售票窗口只有一个,很多人在排队购票。张三买完票离开,轮到李四。李四一直摸不到身份证。当前存在两种解决方法:

  1. 后面的人一直等到李四找到身份证(等待时间不定),直到他购票结束
  2. 请李四去旁边找,后面的人先购票,找到以后再接着购买

千库网_2019春运回家过年火车站售票口人山人海场景_元素编号11631825.png
生活中大多会选择第二种方式,提高效率。这在生活中被叫做“同步”,即李四在找证件的同时,其他人同时做事。
但在 js 中,同步异步的概念则是相反的。

synchronous 同步

即同一时间只能执行一个事情。脚本引擎执行任务时,后一个任务的执行,需等待前一个任务执行完成后再执行。或者说按顺序依次执行。

  1. console.log(1);
  2. console.log(2);
  3. console.log(3);
const arr = [];
for(let i = 0; i < 5; i++){
  arr[i] = i;
}
console.log(arr);

可以发现代码是自上而下依次执行,log 完1 log 2,log 完2 log 3。循环没有执行完,是不会执行 log arr 语句的。这就是同步代码的特点。在程序中,绝大部分代码都是同步代码,而异步代码往往是非常消耗时间的代码。比如发送网络请求获取数据、读取文件等。

假设当前存在这样的需求:

console.log(1);
console.log(2);
// 发送网络请求 | 下载某部电影 | 加载某个文件
console.log(3);

从执行顺序可以猜想出执行结果,log 1-2 后会发送网络请求或下载某部电影或加载某个文件或一些其他比较耗费时间的事儿。但如果当前网络环境不佳,或者服务器出现了一些问题,请求一直在进行中,一直“卡在第三件事”,那么第四件事的 log 3就不会被执行。专业上这样的现象叫做“阻塞”。

asynchronous 异步

即同一时间内做多个任务的一种解决方案。解决方案:

  • 多线程(js 无法实现)
  • 单线程非阻塞,将耗时间的任务(异步任务)交给其他模块处理,处理完成后又交给同步代码来执行

js 是单线程语言,优先处理同步任务,当遇见异步任务,将异步代码丢给异步处理模块,由异步处理模块处理。异步处理模块处理完后,交由任务队列,主线程再从任务队列里获取异步操作的结果。

同步的特点

同步操作的优点在于做任何事都是依次执行、井然有序,不存在“抢资源”的情况。就好比生活中排队买商品、乘车、领取资源,一旦取消了“排队”那么大家势必会争先恐后去抢,造成秩序大乱。

缺点则在于如果某一步骤花费的时间过长,则后续操作都会死等到它完成,从而影响效率。

有一些代码,需要依赖前面代码执行的结果,再进行下一步操作,就好像是工厂流水线作业,生产完蛋糕胚子,再抹奶油、裱花、包装。如果大家都是同时执行,那结果就不一定能正确获取了。

有时希望在效率上能有所提升,也就是说可以让多个操作同时进行,即“异步”。假设现在售票厅有 10 个人需要买票,只有 1 个窗口提供服务,平均每人耗时 5min 那么总共需要 50 min 完成。但如果再加开 9 个窗口,那么当前共有 10 个窗口售票,10 个人只需要共 5min 就能办完。这就是异步代码带来的优势。

千库网_七部门发文助老出行客运售票窗口_元素编号12969533.png

你所见过的异步

分析输出:

setTimeout(function(){
    console.log('异步');
}, 5000);
console.log('同步1');
console.log('同步2');
console.log('同步3');

代码看上去按书写顺序应该输出 异步 - 同步1 - 同步2 - 同步3 吧?但运行结果却会出现 同步1 - 同步2 - 同步3 - (等待到时间后)异步

难道是因为写了时间,“延迟5s再打印”?那代码改为:

setTimeout(function(){
    console.log('异步');
}, 0);
console.log('同步1');
console.log('同步2');
console.log('同步3');

输出结果依然为 同步1 - 同步2 - 同步3 - 异步

原因就是因为 setTimeout 是异步操作。

常见异步任务和任务执行流程

常见异步任务有:

  1. setTimeout()
  2. setInterval()
  3. 文件的读写(IO操作 input output)
  4. 网络请求
  5. 事件

任务执行流程:

  1. 执行同步任务
  2. 如果遇到异步任务,则交给异步处理模块
  3. 异步处理模块将已处理完成的异步任务,扔到任务队列里排队
  4. 当所有同步任务执行完成,从任务队列获取异步的执行结果。或者说等到所有同步任务执行完成后,才会执行异步任务的结果

这里放上个简单的代码图:
111.png
左边为 js 代码,可以发现

  • log 1、log 2、log 3 为同步代码
  • 两个计时器是异步代码
  • 所以把两个计时器放进异步处理模块
  • 执行同步任务所以依次输出了 1 2 3
  • 异步处理模块按时间顺序处理好 log 语句,放进任务队列,1s 的先执行完,2s 的再执行完
  • 输出了同步代码中的 1 2 3 后依次取出 log(‘time’) 和 log(‘out’)

异步的实现

回到这张图来
千库网_七部门发文助老出行客运售票窗口_元素编号12969533.png
这样“开多个窗口”的形式称为多线程,这是异步处理的一种方式,不过这种方式,也是阻塞式的。

开 10 个窗口,可以满足 10 个人同时购票,那 100 个人呢?开 100 个窗口?所以每个窗口实际上还是在排队。就像 1、2 号窗口,除了第一个人在买票,第二个及以后的人,依然处于等待的情况。

也就是说虽然可以开多个线程同时执行多个任务,但每个任务中的代码仍然是同步的。某个任务的代码执行时间过长,会影响到当前线程的进度,不会影响其他线程的代码进度。

单线程非阻塞式

假设现在只有 1 个窗口提供服务,该怎样提高效率呢?

可以把购票流程分为2步:

  1. 预定、付款
  2. 取票

其实现实场景中,我们已经在这样做了。让出行人员在网络上操作第一步,然后直接去火车站取实体票(或者直接凭购票时的证件号码扫码进站)。这样一来最耗时的工作已经提前完成无需排队。这样既提高了效率又开少了窗口。

开一个窗口(单线程)把耗时的操作分为两部分(处理、拿结果)就能保证不阻塞其他代码运行,这就是单线程非阻塞式的异步实现机制。

推荐阅读:
ajax
异步的实现方式