协程粉碎计划 | runBlocking和async启动协程

·  阅读 923
协程粉碎计划 | runBlocking和async启动协程

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

本系列专栏 # Kotlin协程专栏

前言

上一篇文章我们说了使用launch启动协程,做的比喻就是射箭,启动地协程就像射出去地箭,不会返回协程执行结果。那本篇就来看看另外2种启动协程的方法:runBlocking和async。

正文

runBlocking

其实runBlocking从名字就能看的出来,是阻塞的意思,所以它启动的协程特点就是会阻塞线程执行

这里其实可以从2个方面来理解,一个是阻塞线程,比如Android在主线程调用runBlocking函数,这时线程将进入等待状态,等待runBlocking函数执行完成,假如该函数执行太久,就会导致ANR。另一个方式是,在runBlocking启动的协程中,我们使用launch再启动子协程,这时runBlocking会等待子协程执行完成

我们来看个简单例子:

fun main() {
    runBlocking {
        println("coroutine start")
        //模拟耗时
        delay(1000)
        println("coroutine end")
    }
    println("process end")
}

这段代码的执行结果如下:

image.png

会发现线程执行被阻塞了,"process end"必须要等待协程执行完,才可以执行打印。

所以在正常项目中,是特别不推荐使用runBlocking的,建议只在测试中使用。

有返回值

这里或许注意到一点,之前launch启动时,必须要有协程范围,而runBlocking却不需要,我们看一下其方法定义:

public actual fun <T> runBlocking(context: CoroutineContext, 
    block: suspend CoroutineScope.() -> T): T {
    ...
}

会发现它不是CoroutineScope的扩展函数,同时block的函数类型返回值是T,说明是可以返回值的,我们这里来尝试一下:

fun main() {
    val result =runBlocking {
        println("coroutine start")
        //模拟耗时
        delay(1000)
        println("coroutine end")
        return@runBlocking "hello"
    }
    println("process end $result")
}

这里的代码结果如下:

image.png 可以发现result是有结果的。

那有没有即不阻塞线程,又可以获得协程结果的方法呢 那就是async了。

async

既然async方法集成了前面2种的优点,话不多说,直接看下面代码:

fun main() = runBlocking {
    println("coroutine start ${Thread.currentThread().name}")

    val result = async {
        println("inner coroutine start ${Thread.currentThread().name}")
        //模拟耗时
        delay(1000)
        println("inner coroutine end ${Thread.currentThread().name}")
        return@async "fine!"
    }
    println("coroutine continue")
    println("coroutine end ${result.await()}")
}

首先这里会发现之前launch都是使用GlobalScope,但是Kotlin官方不建议使用,所以这里我们使用runBlocking来启动一个协程(虽然runBlocking不建议在项目中使用,但是测试demo可以使用),这样的话该线程就会等待runBlocking执行完成而一直阻塞,免去了写什么Thread.sleep让程序不立马退出的代码。

其次为什么在这里面可以用async启动协程呢,我们可以看一下前面runBlocking的参数block的参数类型,它是CoroutineScope的扩展函数或者成员函数,所以在lambda中已经有了CoroutineScope,所以可以调用async方法,下面是上面代码的执行结果:

image.png

会发现协程1开始后,接着就是调用continue,说明async没有阻塞后续代码执行,然后是协程2开始运行,当调用await()方法时,能获取到结果。

关于await()方法,后面挂起函数文章中会具体说明其实现原理。

我们来看一下async函数的定义:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ...
}

它和launch区别就是block的类型不一样和返回值不一样

钓鱼模型

既然之前launch可以类比与射箭,那async用钓鱼来做比喻就再恰当不过了。

其中Deferred就相当于是鱼竿,我们把鱼饵和鱼钩甩入水中,就相当于协程已经开始工作了,这时主线程可以继续干别的事,当需要返回结果时,再拿起鱼竿,得到钓的鱼(返回结果)。

这个模型可以完美解释非阻塞和拿到返回结果,同时还有一些细节,比如下面代码:

fun main() = runBlocking {
    val deferred: Deferred<String> = async {
        println("In async:${Thread.currentThread().name}")
        delay(1000L) // 模拟耗时操作
        println("In async after delay!")
        return@async "Task completed!"
    }

    // 不再调用 deferred.await()
    delay(2000L)
}

这里我们不再调用await()方法,结果如下:

image.png

会发现启动的协程依旧执行了,这就相当于我钓鱼把鱼钩丢入水中,协程已经运行了,但是我没有抬杆,这个await函数就相当于是抬杆,没有获取结果而已。

阻塞探究

这里说async不完全阻塞也是不对的,原因很简单,当你调用await()方法时,会等待返回结果,也是不会继续方法剩余代码的,但是注意它不会阻塞线程的,就比如你的方法在Android主线程调用async,线程依旧可以绘制UI,只是方法暂时会被挂起不执行,等待一个合适的时机继续执行。

但是它有个很关键的用法,就是我可以同时async多个协程,然后这些协程并发执行耗时操作,这样可以解决串行增加耗时的问题。

所以这里的阻塞可以理解为挂起,只是将这个方法的执行给暂停(挂起)了。

下面有个gif图,可以增强理解一下协程代码执行的顺序:

async.gif

总结

这2篇文章主要介绍了3种启动协程的方法,可以做个简单总结:

  • launch就像是射箭,我们无法直接获取执行结果。
  • runBlocking以阻塞的方式执行协程,会阻塞线程。
  • async就像是钓鱼,即可以不阻塞线程也可以获取到结果。
分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改