查看原文
其他

FIBJS模块重构—从回调到协程

陈垒 Alibaba F2E 2020-01-16

来自FIBJS核心贡献者陈垒在 D2 的演讲 “FIBJS模块重构从回调到协程”。


JS的应用非常广泛,例如做一些浏览器的发展、机器学习、控制机器人以及编写嵌入式的应用。



如上图所示为使用浏览器原生的对象发送一个请求的典型例子。首先创建了一个xhr对象,接下来的一步并不是马上发送它,而是要设一个回调函数onreadystatechange,这个函数表示请求完成以后怎样处理返回结果。xhr也定义了本次请求要发送的HTTP动词和发送的目标网址,直到最后一步才真正将请求发送出去。但是,这个代码是异步的,当send运行之后,代码继续执行,很可能代码没有完成,等到请求完成后才可以处理。像xhr的请求,它的异步过程的执行顺序和声明顺序不一致,先定义回调再发送请求,这种情况称之为异步。因此,要求执行顺序和声明顺序严格一致。现代的应用不是单纯的同步或者异步,往往是两者的混合,时而同步时而异步,所以如何按需处理同步和异步的流程一直是一个热门问题。



也可以使用Promise把异步流程转换到同步,如上图所示。浏览器首先提供了一个fetch方法,发起一个fetch请求,通过then方法将回调处理,推到一个队列里。如果在回调里请求是正确的,返回了HTTP,那么会运行then,如果HTTP请求出错,则会运行catch。但是发完请求之后还是会继续向下执行,对所有的返回结果的处理还是要到then里执行,而上图中的代码是没办法依赖then里的返回结果的,因为不知道什么时候才会执行完成,所以会出现异步的传染性问题,一旦使用回调或promise的方式,能处理异步的方法就变得局限了,只能在then或catch里处理回调的过程。



那么使用async/await + Promise来控制异步会产生什么效果,如上图所示。首先是一个request请求,然后在函数上标记一个async方法,它依然会发起一个fetch请求,与上述唯一不同的是在fetch前加了一个await,这样,在fetch里无论是成功还是失败,总可以得到一个值,这个值会存到result里,当result有值时才会执行。这种方式即处理了异步又可以阻塞上面的流程。但还是会出现异步的传染性问题,await不能用在JS脚本的全局中。



对此,我们可以假想一种新的锁原语,锁对象会阻塞JS的执行,如上图所示。首先设计一个锁,在回调之前先声明一个锁对象,当请求发送出去之后,将锁调用wait方法,作用是等信号量释放,当锁收到不阻塞JS的消息后,继续执行。

 

这样的方法在 NodeJS 和浏览器中都无法执行,但这种风格,确是 FIBJS 推荐的异步控制风格。

 


FIBJS ORM 重构



ORM表示对象关系建模,指数据库之间对象关系的操作。FIBJS自己的包叫FIBJSORM,是Node ORM2包迁移过来的。Node.js引入模块的方式是require,而FIBJS有一个机制,当require一个脚本时,可以指定这个脚本上下文里要做模块解析的别名,所以应用这个机制,让Node ORM2做一个JS上的模块,不需要改代码,只需要替换内置模块,就可以使ORM在FIBJS上运行。但是,存在一个问题,Node ORM2是使用回调编程风格来书写的,因此会消耗性能。



FIBJS 的同步编程风格




如上图所示为FIBJS中的一段代码,其中coroutine表示协程。首先创建了一把锁lock1,然后执行doSthAsync1表示异步的过程,在第9行到第12行,创建了第二把锁,作为第二个异步的事情,在第18行和第19行时,lock1释放的时候再继续向下执行。其中在第5行定义了doSthAsync1回调,在这个回调里锁得到了释放,在第18行收到信号后再继续往下执行。同样,锁2也要等到lock2释放后再继续往下执行。通过这种锁机制的方式,同时尽快的发起了两个异步任务doSthAsync1和doSthAsync2,可以非常确定的知道在18行和19行结束的时候的两个异步任务是完成的,不需要去回调里单独处理结果,也不需要写promise、then、async、await。



当任务过多的时候,可以创建上千个锁,并且在FIBJS中创建锁的代价是很低的,但是代码很繁琐,因此,可以使用上图中的co.parallel函数,它可以创建一个序列化的队列,同时又是并发执行的,并且每个函数的返回结果会返回到一个数组里,再从数组里把函数取出来。上述两种方式就是FIBJS的同步编程风格。



NodeJS 与 FIBJS 编程风格的比较



NodeJS: 回调即异步?



上图所示为NodeJS OMR2处理数据库请求的一个代码。其中connection是一个数据库连接,query执行一条sql,接着在回调里处理下面的逻辑,包括第一个参数err,err不是空的,则将err抛出,后面的字段rows和fields是请求处理得到的结果。



FIBJS: 同步更清爽




首先用rows将代码全部拿出,接着处理相关的逻辑。看似FIBJS的编程风格更同步、更清爽,少了NodeJS中大量的回调过程,同时,回调不等于异步。



如上图所示为Node处理文件写入的一个官方处理方式,当在targetFile中写入内容时,判断执行过程是成功还是失败,可以通过在回调里处理。这样的方式没问题,如果要写十个if write,如果第一个write的结果给第二个write作为一个依赖,要看第一个文件写成功再写第二个,那么就会发生下图所示的情况。这样做是为了提高性能,但是当回调的嵌套层级过多时,性能看似就没有那么好了。




FIBJS ORM 重构前后对比




在重构前,流程控制方式为模块大量使用回调来控制流程,如嵌套层级很深的代码。而重构之后,模块各处采用直接调用返回结果的方式。测试结果在写入十万行数据的时候,FIBJS在重构前比重构后要慢一些。重构前写入100万行时耗时太久,处理平均TPS有4694个,而重构之后可以处理5017个。通过FIBJSORM重构前后的对比,性能优化不是很大,但是可以说明的一点是,不使用回调要比使用回调性能要好。



概念对比




上图是两种编程风格的概念对比,NodeJS的JS线程是一个单线程,FIBJS也是一个单线程。当单线程处理多任务时,NodeJS的选择是事件循环或者多路复用JS主线程,但不建议在JS线程中写繁重的任务。两者都有callback线程,NodeJS异步逻辑控制推荐使用callback或Promise,FIBJS也支持这种方式,但是不建议这样做,建议使用轻量逻辑锁。在处理并发的时候,NodeJS也会使用JSTimer或者Promise.all,但是不推荐FIBJS使用这两种线程,而推荐使用coroutine.parallel处理并发任务,因为它的执行效率更高。



NodeJS中有两种线程,一个是JS的主线程,另一个是工作线程。JS的主线程的全局只有一个,NodeJS的JS线程是一个单线程,同时有多个线程在后台为它工作。但是,通常工作线程不使用,将所有的任务都放到主线程中,这种做法是一种浪费工作线程的表现。



FIBJS也有三种类型的线程,FIBJS也是一个单线程。




把密集计算托管到 Worker 线程




在FIBJS中,把密集计算托管到Worker线程,运行于6C32G的Mac OSX Catalina 15,优化之前,直接将UUID模块放到主线程执行,写入10万行数据的时间比优化后要慢2s左右,写入100万行的数据时,时间差已经扩大到了半分钟,写入1000万行时,时间差已经扩大到分钟,TPS在优化后的提升不是很明显。


总结FIBJS ORM模块重构中运行的两种手段,一是用回调表达异步流程改成直接调用执行。另一种是CPU密集型高频计算,如UUID等,托管到工作线程。以上经验可以完全应用到 NodeJS。



上图为两种编程风格的对比,NodeJS使用node-sqlite3(单线程)来写入数据,FIBJS使用sqlite模块(单线程)来写入数据。对于流程控制方式,NodeJS使用回调的方式,FIBJS使用直接调用返回结果的方式,其它方面都保持一致。当写入10万行数据时,FIBJS只领先了1s左右,写入100万行时,领先了20s左右,写入100万行时,NodeJS的内存直接炸了,平均TPS如上图中数据所示。




FIBJS 的并发性能指标



当随着并发数上升时,从10万到100万时,每个应用的内存占用的趋势如上图所示,nginx非常平滑,fibjs和nodejs两者的走势是相识的,但总体来讲,fibjs占用内存相对比较少。



上图是FIBJS 0.26与早期FIBJS的对比。其中last表示0.26的版本,newfiber表示0.27版本。FIBJS在任务调度上力求充分利用系统的线程。



上图所示,当在Google搜索nodejscallback hell时,有8万多个词条,说明NodeJS里的回调风格仅仅在表达上给很多人造成了困扰,相对FIBJS的同步执行可能不是最终或者更好的答案,至于使用哪种风格表达异步流程,是见人见智的问题。但是,FIBJS在NodeJS之外提供了另外一种异步编程的风格,回调带来的问题在FIBJS不会发生,同时,通过上文数据可以看出FIBJS表现的更好,内存管理也更好。



NodeJS 生态现状



Npm社区大部分的包都是给NodeJS服务的,包的总数量如上图所示,NodeJS有专门的委员会、完整的开源流程,并且API稳定,同时也是前端不可或缺的工具,与 php/.Net/cocoapods等生态联动极多,被世界上大多数公司采用。使用NodeJS可以做出高性能的案例,但是过程是相当痛苦的,世界上已经有非常成功的应用案例。



FIBJS 生态现状



FIBJS兼容NodeJS包管理机制,通过SandBox可使用大部分npm包,目前独属于FIBJS的包还比较少,主要用于游戏服务器、云平台后端、区块链开发、嵌入式开发,但是有个很大的问题是FIBJS的国际化不足,尚无知名国际应用案例。


 

 

你可能还喜欢👇👇

干货 | 第十四届 D2 前端技术论坛 20+ 份精彩演讲 PPT 分享

基于浏览器的实时构建探索之路

前端工程化下一站: IDE

Serverless 函数应用架构升级

数据分析的人工智能画板—马良 

标准微前端架构在蚂蚁的落地实践 







关注「Alibaba F2E」

把握阿里巴巴前端新动向



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存