Harmonyos next:浅析async、await&类比Android的协程

623 阅读4分钟

Promise和async/await提供异步并发能力,是标准的JS异步语法。异步代码会被挂起并在之后继续执行,同一时间只有一段代码执行,适用于单次I/O任务的场景开发,例如一次网络请求、一次文件读写等操作。无需另外启动线程执行。

异步语法是一种编程语言的特性,允许程序在执行某些操作时不必等待其完成,而是可以继续执行其他操作。

一、基本用法

1.1鸿蒙

  async fetchData() {
    const value1 = await PreferenceManager.getInstance().getValue<xxx>(AccountModel.xxx)
    const value2 = await PreferenceManager.getInstance().getValue<xxx>(AccountModel.xx)
    const value3 = await PreferenceManager.getInstance().getValue<xxx>(AccountModel.xx)
  }

1.2安卓

    fun fetchData() {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                Thread.sleep(1000)
                Log.d("bai", "1111111111")
            }
            withContext(Dispatchers.IO) {
                Thread.sleep(1000)
                Log.d("bai", "2222222")
            }
            withContext(Dispatchers.IO) {
                Thread.sleep(1000)
                Log.d("bai", "333333")
            }
        }
    }

1.3对比

用法上相差不大,鸿蒙更简洁一些。

鸿蒙直接给函数标记async,然后在函数内可以await阻塞耗时操作返回再往下执行。

安卓的协程则需要在协程作用域内使用,函数需要用suspend标记,也需要切线程来执行耗时操作。

需要注意的是鸿蒙如果想要调用async函数的await,需要该函数也是async。比如上面的fetchData,如果有个loadData函数想要调用fetchData的await,那么loadData也必须是async标记,如下:

​
  async loadData(){
    await this.fetchData()
  }

如果loadData函数不适合用async标记,那么可以改成用回调的形式使用

   loadData(){
    this.fetchData().then(()=>{
      
    })
  }

到这里,其实可以看出来async、await解决的其中一个问题:回调地狱。

试想一下,如果有个场景是先请求A,用A请求B,用B请求C,用C请求D.....用then的话,需要嵌套多少层回调。

如果用async、await只是多几行await罢了

  async loadData() {
    const value1 = await this.fetchData(0)
    const value2 = await this.fetchData(value1)
    const value3 = await this.fetchData(value2)
    const value4 = await this.fetchData(value3)
  }

二、原理浅析

2.1鸿蒙的async、await

由于没看到鸿蒙的源码,鸿蒙的async、await看起来又是js的,因此套用js的原理。

async函数返回的是一个promise对象,通过await去取promise的值。

在 JavaScript 的运行时环境中,基于事件循环(Event Loop)机制来处理异步任务。await 暂停 async 函数执行时,实际上是将后续代码的执行权暂时交出去,等待对应的 Promise 进入微任务队列(Promise 的 thencatch 等回调在成功或失败后会被添加到微任务队列中)并执行完微任务队列中的相关任务后,才会恢复 async 函数的执行。

2.2安卓的协程

安卓的协程由于有源码可以看,因此相对比较清晰。

总的来说协程有两个比较重要的元素:续体continuation和状态机。

续体用来回调恢复,保存上下文。状态机用来恢复时控制继续执行哪些逻辑。

suspend方法在编译成java代码时会塞一个Continuation续体参数给对应函数(其实通过查看源码retrofit对协程的支持,能看到retrofit就是通过代理函数的最后一个参数拿到的continuation)

每当代码执行到挂起点时,函数直接return返回,并且把续体传递给下一个挂起函数,相当于本函数暂时执行完了,也就不会阻塞当前线程如主线程了。

企业微信截图_17350109873636.png

续体可以简单理解成回调,包含了上下文的回调,等挂起函数执行完成后通过续体把上一个函数唤起继续执行,状态机进入下一个状态(如果没有挂起点就可以返回结果了)。

企业微信截图_17350111222478.png 上图的lable是记录状态的,默认0,执行完成后自增1,遇到挂起点直接返回,等continuation的resume唤醒,执行下一个状态。

用安卓的retrofit对协程支持的源码也能比较明显的看出来协程续体的作用

企业微信截图_17350104085335.png

续体其实可以理解成回调,resume对应onSuccess,resumeWithException对应onFaile

2.3对比

从安卓的协程对比鸿蒙的promise,看起来实现的效果是类似的,特别是KMP(kotlin跨端方案)已经有类似的鸿蒙跨端方案,把kotloin的协程转换成鸿蒙的promise。

但是原理应该不一样。安卓协程是在主线程上不能做耗时操作,如果执行了thread.sleep(6000),就会ANR,正确做法需要切到io线程操作。

对应鸿蒙的场景直接耗时操作就没发现出问题,可能是因为js本身单线程微队列的实现原理不一样,这块等后面深入再探究。