深入浅出Node.js--异步编程有感

七月初和家人出去度假,大约九天时间。世界那么大,多看看,遇见不同的人和事,总是多有裨益。回来后,本周又看了深入浅出nodejs关于异步编程的部分,感觉有新的体会,和大家探讨。

异步编程-what

抛开Node.js的事件循环和线程池相关的概念,我们首先思考下什么是异步编程。如果一时不知从何说起,可以先想一想相对应的所谓“同步”是什么意思:同步意味着顺序执行,比如N个任务依次按序执行,前一个执行完毕才可以执行下一个。非顺序执行的就是异步,异步执行的情况下,下一个任务开始并不需要前一个任务执行完毕。从这个角度看,异步执行必定是在多线程的环境。简单地总结一下,同步/异步针对的是应用层,区别在于一个任务开始执行是否需要前一个任务执行完毕。
接着这个话题,有必要再说一说阻塞和非阻塞。Nodejs中,异步要解决的问题主要来自于IO造成的损耗。阻塞IO意味着IO必须彻底完成才能返回,在返回之前CPU需要等待——也就是说,此时别的什么都干不了。非阻塞IO不等完成就可以返回。阻塞非阻塞针对的是系统级,因此说到非阻塞并不能直接推断出应用是否异步,因为两者不是一个层次的概念。借用书中的图例:

阻塞和非阻塞 IO

从上可以看出,在非阻塞IO调用的时候,应用层也可以是同步的。但正是有非阻塞IO,才给了异步编程可能。

Nodejs的异步编程实现

伴随异步编程而来需要解决的问题就是任务执行成功后,如何让应用层知道。非阻塞IO可以立即返回,应用层从而可以进行下一个任务。但是,如果要获取非阻塞调用的结果,就需要使用轮询技术。目前,epoll是Linux下效率最高的利用事件通知实现轮询的技术:

epoll

如上所示,在等待的时候,应用层需要休眠,所以也是一种同步。但基于事件的轮询,可以实现异步编程。Node.js给出的方案就是 IO线程池+事件循环。在主线程中执行任务,任务的真正执行交由IO线程池。与此同时,Node.js中始终存在一个事件循环,每一次循环会检查是否有完成的事件,并执行相关的回调函数。而线程池的实现,*nix上使用自定义的线程池,Windows上使用IOCP,node.js使用libuv作为不同平台线程池的抽象,来实现统一调度。
这个不算复杂的运行逻辑,继续深挖,可以扩展出很多问题:

  1. 线程池:数目如何确定;任务如何分配给线程
  2. 事件:事件循环什么时候开始;如何判断任务完成;如何通知应用层的调用方;不同调用方是否存在优先级

以上问题留待读者看书解决了。

异步编程需要注意的地方

结合实际,异步编程会遇到的问题主要有:

  1. 不能正确地使用 try/catch
  2. 嵌套太深带来业务处理的麻烦
  3. 如何正确地设置延时
  4. 任务之间有依赖时,如何正确地调用执行

我们逐一来看这些问题。try-catch主要指的是当回调函数出错的时候,try-catch是捕捉不到的,这也是为什么回调函数的参数会带error。
嵌套太深带来的问题不止是丑陋的缩进,更可能造成内存泄漏。使用 aync/await基本可以解决这个问题。
使用setTimeout结合async/await可以实现延时,比如:

let a = async function test(){
    console.log(1)
    await sleep(1000)
    console.log(2)
    console.log(3)
}
function sleet(ms){
    return new Promise(resolve => {
        setTimeout(resolve, ms)
    })
}
a()

要注意和以下代码的区别:

function function1() {
    console.log('func 1');
}

function function2() {
    console.log('func 2');
}

function function3() {
    console.log('func 3');
}
function1();
setTimeout(function2, 3000);
function3();

最后就是涉及到的流程控制问题,我认为主要可以分为两类:单依赖和多依赖。单依赖指的是任务依赖一个任务,多依赖指的是任务依赖多个任务。对于单依赖,现在的Promise支持链式调用,原生地完美解决了这个问题。而多依赖,使用async/await将异步流程转换成同步流程或者使用Event,也可以较好地实现这种业务流程。另外,书中提到了一些实现控制流的第三方库,从其中可以借鉴开发思路。

参考

Async/Await 学习 深入浅出Node.js