和少妇白洁一块儿学JavaScript之Async/Await

能和微博上的 @响马 (fibjs做者)掰扯这个问题是个人荣幸。javascript

事情缘起于知乎上的一个热贴,诸神都发表了意见:java

https://www.zhihu.com/questio...node

这一篇不是要说明白什么是async/await,而是阐述为何会在编程技术这么多年后出现和流行了这个东西,读懂这篇文章你须要对async/await有很透彻的机制理解。程序员

若是是写系统程序,流行的编程范式是面向对象,这很是成熟不用多说;但若是是写微服务(restful api server),状况不一样。算法

写微服务的时候数据不是从文件或数据库读取、去串行化、构造对象而后在内存中维护对象;而是向数据库、cache、或者API提取数据,计算后尽快输出结果;数据库

前者的数据对象生命周期较长,object-oriented范式很合适,它研究一个对象的状态机和如何响应外部事件;编程

后者的数据生命周期很短,并且更糟糕的,各类input数据的结构也不很稳定,常常变化,因此这个时候OO的模式就显得笨重和低效了,在这个时候对data的处理不是object-oriented范式,而是transformation-oriented范式。api

后者致使了函数式编程的兴起,这里无法仔细讨论函数式编程的方方面面,咱们仅仅说transformation的问题。promise

这种编程范式下一次api服务的生命周期在心理模型上一个函数的开始和结束,这个函数须要从不少地方pull数据,若是是从内存中直接pull,这个在fp里叫作state monad;若是是异步pull数据,包括文件、数据库、其余api,这个叫io monad。性能优化

OO的本质站在fp的角度看是如何维护state monad,若是程序中有stateful的部分,或多或少都会有,用oo建模不是问题;访问这些state都是同步的也不是问题;

async/await的出现是为了解决第二个问题,io monad。

在采用transformation和fp方式写微服务的时候,常见状况不是处理单一数据单元,而是数据集合,集合数据的变换是map/filter,聚合是reduce(广义);这个过程能够有条件,能够是nested,其结构取决于你的业务逻辑和solution model,不是编程技术解决的。

因此你大致能够把这些逻辑先用同步的方式写出来,假定全部异步得到的数据均可以同步得到,而后把须要pull的数据改为用async/await去获取;这在结构上很清晰;

在这个时候开发者考虑的问题不是如何对付单一数据的异步获取问题,而是考虑这些异步过程之间如何去串行和并发的问题;换句话说,他们的执行序是你要program的逻辑的一部分。既然他们是programming逻辑的一部分,那么他们显示存在就理所应当。

这里说的串行和并发仅指从io monad里pull数据的操做,不是指程序中其余部分的执行体之间的并发或并行。下同。

这里有两个平衡:

第一:若是要追求service time越短越好,也就是提升响应时间,那么这些异步就会象project软件里的甘特图同样,能并发的尽早并发,service time取决于最长的路径。一般瓶颈都是io不是算力,除非设计有问题或者算法写得太烂。

这种优化极可能带来代码结构的不清晰,可是它是能够作并且容易作的,在async/await模式下,由于它在代码层面上基本上保留了这个甘特图关系。

它适应业务变化的能力也很好,在业务逻辑变化必须修改的时候,开发者总有一个比较清除的甘特图,若是你不在async子函数里封装太长的没必要要逻辑的话;和OO建模时咱们反复问一个对象是否是single responsibility同样,一个async函数的封装越原子化,越容易让开发者在上层组合顺序和并发。

这里我不去批判thread或者fiber或者goroutine或者coroutine的模型,只强调异步数据的pull逻辑的原子化,这是高并发微服务编程对开发者提出来的新问题,原则上任何一种开发语言和开发模型均可以作到对等的性能和可用性,但实践上大多数状况下,程序员不把program异步pull数据的顺序和并发当成是本身编程逻辑的一部分,去享受thread model下的编程逻辑简单,这是不对的;你能够有理由不急着去作service time优化,可是不意味着你根本不知道它的模型逻辑和若是要去优化,作法是什么。

第二:async函数对gc的压力很大,由于compiler很难去判断在运行时哪些域内变量能够回收,这不一样于闭包变量,闭包变量的生命周期判断在源码级的词法域就能够分析出来;因此async函数的执行应该是短生命周期的。

例子

贴一小段代码,实际项目代码,没什么特别的,Promise用了bluebird库:

async storeDirAsync(dir) {

    let entries = await fs.readdirAsync(dir)
    let treeEntries = await Promise
      .map(entries, async entry => {
          
        let entryPath = path.join(dir, entry)
        let stat = await fs.lstatAync(entryPath)
            
        if (stat.isDirectory())
          return ['tree', entry, await this.storeDirAsync(entryPath)]
            
        if (stat.isFile())
          return ['blob', entry, await this.storeFileAsync(entryPath)]

        return null
      })  
      .filter(treeEntry => !!treeEntry)

    return await this.storeObject(treeEntries)
  }

这是一个class方法。

它的第一步是获取了一个文件夹内的entries,而后用Bluebird库提供的map方法应用了一个async函数上去,这是个匿名函数。

匿名函数是咱们喜欢fp的一个重要缘由,functor chaining也是,它们分别消除了不少代码细节上须要命名变量名或函数名的须要。

这个匿名函数内,有更多的await操做,根据fs.stat的结果针对目录和文件作了不一样处理,并且有递归。async以内是顺序执行的,但async在map里是并发的,这些东西都显式摆在代码层面上。

若是任务范围更大,你能够把不少promise聚合在尽量早的时候并发。

固然这个写法没有美好到能够直接写entries.mapAsync()的程度,但基本上作到了上述的要求:在源码层面上对顺序和并发有一览,有控制,容易变动。

说到底,async是让这种顺序和并发的书写和维护变得容易,而不是说我不要写并发,一切顺序走;可是反过来讲它的效率不是最好的,在node里最好的效率目前和可见的将来都是裸写callback,那是最后的性能优化了。

最后咱们说这个写法的一个有点麻烦的坑。

在class方法里写async有个this binding的问题,搞出来一个闭包变量并非最好的办法,Bluebird库里有Promise.bind方法解决这个问题,上述代码中用arrow function的lexical scope bind this也是一个办法(也是推荐的办法)。

总结

node.js是我写过的最好的纯粹event model模型的开发环境;远好过天生thread模型倒回来打不少non-blocking补丁的作法;

javascript领域,和目前整个编程界,在使用asynchronous(异步)这个词来讲咱们在这篇文章里聊的问题,这是个错误,asynchronous在编程上有其余含义,不管是写系统程序(signal handler)仍是写内核或者裸金属(isr);这个问题的准确表述是:non-blocking。

而对应non-blocking的solution模型是如何调度(schedule)执行体;再而后的问题转换成你须要显式调度仍是隐式调度?

若是你认为:

  1. service time是须要追求的

  2. 调度逻辑是常常随着业务逻辑变化而变化的

  3. 完整的数据流变换逻辑和调度逻辑都应该在代码层面上呈现总览,是top-down的构建的

你应该选择async/await;

反之,你但愿编程极致简单,调度不在你的solution模型以内,你bottom-up构建逻辑,应该远离javascript,选择thread模型。

白洁

“请把你的左手放在本身的大咪咪上,回答一个问题,调度执行体和调度io是一回事吗?”

白洁摇摇头。

“我也认为不是,可是不少runtime library并无区分二者。” said I.

JavaScript的event model并无所谓的调度执行体的设计,它本质上只有调度io。

相关文章
相关标签/搜索