单线程开发异步任务?ACE JS框架是如何实现的

开发 开发工具
HarmonyOS ACE开发框架遵循EventLoop调度机制,并且提供了更多的机制和API,让业务逻辑可以在外部线程执行,通过消息通信机制让JS语言有了异步的属性。

做应用开发时,开发语言的选择至关重要。例如Java语言的线程特性能够让多任务并行,从而充分利用硬件资源开发出高性能的应用。而HarmonyOS 2提供的应用开发语言中,不仅有多线程的Java语言,还有单线程的JS语言。

那JS语言该如何充分利用硬件资源开发出高性能的应用呢?为此,HarmonyOS提出了“ACE JS的单线程异步机制”来解决这一问题。

虽然,JS语言本身是无法实现异步功能,但是ACE JS框架提供了多线程的宿主环境,通过消息通信机制让JS语言有了异步的属性,接下来我们来了解下具体的实现原理。

ACE开发框架

使用JS开发HarmonyOS应用,使用的开发框架名为ACE(Ability Cross-Platform Environment),该框架适用于手机、平板、智慧屏、智慧表、车机等设备,具备“一次开发,多端部署”的能力。

ACE框架包括应用层(Application)、前端框架层(Framework)、引擎层(Engine)和平台适配层(Porting Layer),如下图所示:

  • Application:应用层表示开发者使用JS UI框架开发的FA应用,这里的FA应用特指JS FA应用;
  • Framework:前端框架层主要完成前端页面解析,以及提供MVVM(Model-View-ViewModel)开发模式、页面路由机制和自定义组件等能力;
  • Engine:引擎层主要提供动画解析、DOM(Document Object Model)树构建、布局计算、渲染命令构建与绘制、事件管理等能力;
  • Porting Layer:适配层主要完成对平台层进行抽象,提供抽象接口,可以对接到系统平台。比如:事件对接、渲染管线对接和系统生命周期对接等。

ACE开发框架的线程模型

每个HarmonyOS JS应用,都是通过上图所示的ACE开发框架进行加载渲染的。ACE开发框架包含了JS线程、UI线程、GPU线程、IO线程,并且在ACE框架外还会存在一类后台任务线程。其中GPU线程与IO线程为ACE框架内部的专有线程,主要作用于ACE框架初始化与页面加载渲染的过程;UI线程、JS线程和后台任务线程会与应用开发代码相关:

  • UI线程:负责应用界面的绘制刷新,与应用的进程号相同,又叫主线程。如果开发JS+JAVA的混合编程,JAVA PA(Particle Ability)的onStart/onConnect等Ability生命周期回调便是运行在主线程,若在这些生命周期回调上执行耗时操作则会导致JS UI的绘制刷新卡住。
  • JS线程:应用的JS代码会被JS引擎解析执行,并运行在JS线程上,而JS又是单线程语言,所以目前我们工程中看到的所有的JS代码都会执行在这个进程下唯一的JS线程上。
  • 后台任务线程:这里是对ACE框架外部的后台线程的一个统称,并不单指一个线程,也并不唯一。后台任务线程包含了Java PA线程、文件操作API、网络访问API内部实现等相关线程。

下面我们结合测试代码,分析这三个线程的作用和关系。

JS线程与UI线程的关系

为了验证JS线程与UI线程的关系,我们准备了一个实验性质的Demo,主要代码以及运行过程的Log如下:

首先我们在IDE建立一个Empty Ablity(JS)模板的HelloWorld工程,在生命周期、按钮响应回调方法里增加Log以观察线程情况。刚创建的app.js中Application生命周期默认已经有Log,无需额外添加。

我们只需要在主界面index.js文件中onInit增加日志:

  1. console.info('page.default onInit'); 


然后在index.hml中增加一个button以及会一直进行动画的progress组件:

  1. <button id='button1' onclick="onButtonClick">I'm a button</button> 
  2. <progress type="circular"/> 


最后在index.js中增加按钮点击响应事件以及Log,并且尝试sleep阻塞js线程:

  1. function sleep(delay) { 
  2. for (var t = Date.now(); Date.now() - t <= delay; ); 
  3. onButtonClick() { 
  4. console.info('onButtonClick begin'); 
  5. sleep(1000); 
  6. console.info('onButtonClick end'); 


将应用运行起来,点击两次按钮,得到如下Log:

从输出的Log中,我们可以看到这个JS FA进程号为22592,也就是说UI线程是22592;生命周期回调以及按钮响应均在24077线程,这个就是JS线程,所以JS线程与UI线程不是同一个线程。

并且我们尝试通过sleep方法阻塞JS线程,想观察JS线程阻塞是否会影响到UI线程的刷新。最终得出的结论是无论JS线程sleep多长时间,UI界面上的progress组件动画一直会不断刷新,按钮也会有按压效果变化,所以我们可以推测JS线程与UI线程的相互调用应该是通过某种消息机制完成的,而不是阻塞式的调用。

JS线程与后台任务线程的关系

ACE JS框架提供了JS FA(Feature Ability)调用Java PA(Particle Ability)的机制,该机制提供了一种通道来传递方法调用、处理数据返回以及订阅事件上报。通过以下Demo可以验证 JS 线程与Java PA线程的关系:

在JS中,我们通过FeatureAbility.callAbility拉起并调用了名为一个类名为ServiceAbility的Java PA,并拿到返回结果:

  1. var action = {}; 
  2. action.bundleName = 'com.blancwu.test'
  3. action.abilityName = 'com.blancwu.test.ServiceAbility'
  4. action.messageCode = 1001
  5. action.abilityType = 0
  6. action.syncOption = 0
  7.  
  8. console.info('FeatureAbility.callAbility begin' + JSON.stringify(action));FeatureAbility.callAbility(action).then(function (value) { 
  9. console.info('FeatureAbility.callAbility async result ' + JSON.stringify(value)); 
  10. }) 
  11. console.info('FeatureAbility.callAbility end' + JSON.stringify(action)); 

在ServiceAbility的onRemoteRequest中增加Log输出,并sleep 1秒钟,以便观察线程情况与之间关系:

  1. @Override 
  2. public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) throws RemoteException { 
  3. HiLog.info(LABEL_LOG, "onRemoteRequest begin " + code); 
  4. if (code == 1001) { 
  5. try { 
  6. Thread.sleep(1000); 
  7. } catch (InterruptedException e) { 
  8. e.printStackTrace(); 
  9. Map<String, Object> result = new HashMap<String, Object>(); 
  10. result.put("result", 1); 
  11. reply.writeString(ZSONObject.toZSONString(result)); 
  12. HiLog.info(LABEL_LOG, "onRemoteRequest end " + code); 
  13. return super.onRemoteRequest(code, data, reply, option); 


完成以上代码后,并进行执行,可得到的Log如下:

我们观察到本次运行主进程(UI线程)号为4133,JS代码执行在JS线程5887,Java PA响应onRemoteRequest执行在另一个后台任务线程5837。通过Log我们看到onRemoteRequst即使阻塞了后台任务线程1s也不会影响JS线程的并行执行以及主线程(UI线程)上动画的刷新,做到了JS线程与后台任务线程异步地执行事务。

JS线程的异步机制

上面从代码实验角度观察到了JS线程与其他线程的异步关系,那么JS线程处理来自其他多个线程的调用是怎么实现的呢?
首先,我们来看一下传统浏览器环境下的运行机制:

上图中,JS线程中的函数调用会存在于栈(stack)中,栈中的函数可以调用浏览器环境提供的WebAPIs,包含了DOM、ajax、timeout等API,这些API会在浏览器环境提供的另外一个外部线程执行,执行完成后会在任务队列(callback queue)中加入对应的回调事件(如onClick、onLoad、onDone)。

当栈中的代码执行完毕,即栈清空后,JS线程又会通过event loop取出任务队列中的下一个任务进行执行,以此类推完成整个程序执行。

HarmonyOS ACE开发框架同样遵循上述最基本的EventLoop调度机制,并且提供了更多的机制和API,让业务逻辑可以在外部线程执行,包含了上面提到的Java PA以及异步回调的系统能力API。其中,异步回调的系统能力API包含如文件系统操作和网络操作等,具体大家可以按照我们实验Demo的方法去尝试一下。
● 参考 https://developer.harmonyos.com/cn/docs/documentation/doc-references/js-apis-file-storage-0000000000629445

未来发展的展望

目前ACE JS应用内实现多线程的最佳方式是通过混合编程调用Java PA方式,未来,随着纯JS应用越来越多,只支持单线程的JS ACE框架的异步API并不能解决各种复杂场景问题。

单线程的JS加上异步API能够很好解决单个I/O阻塞的问题,但是如果遇到大量的I/O事件,比如批删除大量文件,通过for循环发起了大量异步任务,也会降低执行效率,甚至阻塞其他异步任务的执行。并且如果要使用JS语言开发计算密集型的任务,也无法在唯一的JS线程上进行。

这时就需要一个真正的JS多线程处理机制了,虽然目前HarmonyOS 2还未支持,但未来HarmonyOS会考虑规划出与HTML5类似提供支持WebWorker机制,支持开发出多线程的JS代码,提供给应用开发者更多的发挥空间。

 

责任编辑:黄显东 来源: 51CTO
相关推荐

2021-01-10 11:21:33

JavaScript语言开发

2009-07-10 09:05:20

SwingWorker

2021-06-11 11:28:22

多线程fork单线程

2019-06-17 14:20:51

Redis数据库Java

2020-11-09 09:33:37

多线程

2022-06-22 08:16:29

异步非阻塞框架

2021-08-10 07:00:01

Redis单线程并发

2010-08-30 08:55:56

JavaScript引

2022-01-04 11:11:32

Redis单线程Reactor

2022-07-07 09:12:17

JavaScript线程循环机制

2019-05-06 11:12:18

Redis高并发单线程

2022-07-18 13:59:43

Redis单线程进程

2020-10-26 08:55:52

Redis单线程模型

2010-01-28 16:45:44

Android单线程模

2024-02-26 00:00:00

JavaScript单线程高效

2017-03-06 14:08:38

JavaScript单线程setTimeout

2023-10-15 12:23:10

单线程Redis

2019-11-25 10:13:52

Redis单线程I

2024-09-27 11:51:33

Redis多线程单线程

2012-02-15 10:26:40

JavaJava Socket
点赞
收藏

51CTO技术栈公众号