4.协程-启动&分析执行过程

Posted 黄毛火烧雪下

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4.协程-启动&分析执行过程相关的知识,希望对你有一定的参考价值。

前面我们将 suspend 函数拔了个精光。让我们知道了 suspend 的功能,以及如何定义。经过前面的知识积累,我们接下来学习协程就很轻松了,打起精神!!

runBlocking

runBlocking 是什么呢?还记得我以前的代码如何写的吗?

fun main() {
    GlobalScope.launch {
        delay(3000)
        print("开始执行")
    }
    Thread.currentThread().join()
}

可以注意到上方的代码,我调用了 Thread.currentThread().join() 阻塞了主线程。原因就是 GlobalScope.launch 启动协程的代码实在其他线程做事情,如果主线程执行完毕了,其他线程也会自行结束。

哪有没有能启动协程,并且当内部协程都执行完毕了,在结束线程呢? runBlocking 就是具有这种功能。我们改造下上方代码。

fun main() = runBlocking<Unit> {
    launch {
        delay(3000)
        print("开始执行")
    }
}

上方代码结果是在 3 秒后,输出 开始执行 ,且之后主线程才会结束。接下来我们来看下官方是如何解释 runBlocking 的。

1.调用了 runBlocking 的主线程会一直 阻塞 直到 runBlocking 内部的协程执行完毕。
2.这里的 runBlocking { …… } 作为用来启动顶层主协程的适配器。 我们显式指定了其返回类型 Unit,因为在 Kotlin 中 main 函数必须返回 Unit 类型。

第一句我们现在能理解 runBlocking 具有阻塞线程的功能,一直到内部的协程都执行完毕的时候才会结束。
第二句我们注意到官方说了一个 runBlocking 作为用来启动顶层主协程的适配器 。主协程?难道协程还具有继承关系?没错协程的确具有子父协程关系,这种关系如果理解不透彻会让你出现内存泄漏的情况,这个问题我们后续讲,大家先记住这个点。

这里记录一个问题,协程子父关系如何来确定。

协程的执行顺序

接下来我们看下面的的代码,大会思考一个问题:2 个输出语句的时间间隔相差几秒呢?

fun main() = runBlocking<Unit> {
    // 启动协程 A
    launch {
        delay(2000)
        println("开始执行:${System.currentTimeMillis()}")
    }

    // 启动协程 B
    launch {
        delay(1000)
        println("开始执行2:${System.currentTimeMillis()}")
    }
}


输出结果:  
开始执行2:1577759541323
开始执行: 1577759542321

我们注意到输出的时间 2 个协程是时间相差间隔是 1 秒,并且 协程B 优先于 协程A 执行完成。难道 2 个协程是并行的吗?如果按照 Java 的思想去考虑,协程 A 与 B 执行的代码,应该处于 2 个线程中,我们尝试输出下线程。

fun main() = runBlocking<Unit> {
    // 启动协程 1
    println("1.线程昵称:${Thread.currentThread().name}")
    launch {
        println("2.线程昵称:${Thread.currentThread().name}")
        delay(2000)
        println("开始执行:${System.currentTimeMillis()}")
    }

    // 启动协程 2
    launch {
        println("3.线程昵称:${Thread.currentThread().name}")
        delay(1000)
        println("开始执行2:${System.currentTimeMillis()}")
    }
}

输出结果:
1.线程昵称:main
2.线程昵称:main
3.线程昵称:main
开始执行2:1577760198999
开始执行:1577760199994

在这里插入图片描述
我们注意到 3 个线程都是主线程,那 A 与 B 协程是如何做到并行的呢?
别着急,接下来我给大家细细的分析下,这 2 个协程的执行过程,并且给大家从这里温习非阻塞式挂起。
在这里插入图片描述
根据上图的我们就能分析出协程的执行流程了,以及非阻塞挂起了。
在这里插入图片描述

不信?那咋们代码上过招。

fun main() = runBlocking<Unit> {
    // 启动协程 1
    println("1")
    launch {
        println("2")
        delay(2000)
    }

    // 启动协程 2
    launch {
        println("3")
        delay(1000)
    }
    println("4")
}

大伙猜猜上面的代码输出结果是啥呢?如果按照我上面的流程图,结果应该是 1->4->2->3。

launch

前面经常使用 launch 启动一个协程,接下来我们来看看 launch 还能干吗?接下来我们来揭开它的头盖骨。

首先看下 CoroutineScope.launch 的构造函数。

这里我们注意 2 点 launch 函数的 start 参数,以及返回参数 Job(至于其他参数后续讲)。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ....
}

Job对象

Job 主要有 2 个功能:

  • 1.等待协程执行完毕 join 方法。
  • 2.取消协程 cancel 方法。
fun main() = runBlocking<Unit> {
    // 启动协程 A
    launch {
        delay(2000)
        println("开始执行:${System.currentTimeMillis()}")
    }

    // 启动协程 B
    launch {
        delay(1000)
        println("开始执行2:${System.currentTimeMillis()}")
    }
}

回顾下上方代码,如果我想等待 A 执行完毕,在启动协程 B 要如何操作呢?其实就用 join 方法即可

fun main() = runBlocking<Unit> {
    // 启动协程 1
    val jobA = launch {
        delay(2000)
        println("开始执行:${System.currentTimeMillis()}")
    }
    jobA.join()
    // 启动协程 2
    launch {
        delay(1000)
        println("开始执行2:${System.currentTimeMillis()}")
    }
}

同理 cancel 也一样,我这里就不贴代码啦!但是 cancel 协程会有一个异常,这个异常是可以不捕获的,后续的协程异常篇章在讲解这个。

CoroutineStart对象

我们注意到 launch 还具有一个 start 参数,默认参数是 CoroutineStart.DEFAULT。这个参数有何作用呢?
还记得我讲解协程执行顺序的时候,大伙有没有注意,当启动 A 与 B 协程后,协程 A 的代码就自动执行了,为何会自动执行呢?其实就和这个参数有关。
在这里插入图片描述
我们来看下 CoroutineStart 为我们提供了什么类型:

public enum class CoroutineStart {
    // 默认类型,自动执行
    DEFAULT,
    // 懒加载,当调用 join 方法才会执行
    LAZY,
    // 不知道,忘记啦,自己查
    @ExperimentalCoroutinesApi
    ATOMIC,
    // 不知道,忘记啦,自己查
    @ExperimentalCoroutinesApi
    UNDISPATCHED;

    ...
}

接下来我们尝试 start 改成 CoroutineStart.LAZY 。

fun main() = runBlocking<Unit> {
    // 启动协程 A
    launch(start = CoroutineStart.LAZY) {
        println("开始执行协程A")
        delay(2000)
        println("开始执行:${System.currentTimeMillis()}")
    }
    // 启动协程 B
    launch(start = CoroutineStart.LAZY) {
        println("开始执行协程B")
        delay(1000)
        println("开始执行2:${System.currentTimeMillis()}")
    }
    println("等待协程执行完毕!")
}
输出结果:
等待协程执行完毕!

我们发现只输出了一句话 等待协程执行完毕! ,而且主线程一直处于阻塞状态,协程 A 与 B 的代码都没执行。由于 start的模式是 CoroutineStart.LAZY 只有在调用对应 Job 对象的 join 方法才会执行。

fun main() = runBlocking<Unit> {
    // 启动协程 A
    val jobA = launch(start = CoroutineStart.LAZY) {
        println("开始执行协程A")
        delay(2000)
        println("开始执行:${System.currentTimeMillis()}")
    }
    // 启动协程 B
    val jobB =  launch(start = CoroutineStart.LAZY) {
        println("开始执行协程B")
        delay(1000)
        println("开始执行2:${System.currentTimeMillis()}")
    }
    println("等待协程执行完毕!")
    jobA.join()
    jobB.join()
}
输出结果:
等待协程执行完毕!
开始执行协程A
开始执行:1577763305539
开始执行协程B
开始执行2:1577763306544

可以看到 A 和 B 协程都执行了,

async

对比学习 launch 以外,还有一个 async 它大体的功能和 launch一样,但是它具有能获取返回值的功能。一般来说我们异步操作就是为了做耗时操作获取数据,因此 async 来启动协程比较常用,我们来看下它的源码吧。

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

发现和 launch 几乎一样,只是返回的对象是 Deferred 而不是 Job,其实 Deferred 是 Job 的子类,用法也很简单,若要获取返回值只需要调用 Deferred 的 await 方法即可。

fun main() = runBlocking<Unit> {
    // (1)启动协程 A
    val deferredA = async{
        println("开始执行协程A")
        delay(2000)
        val time = System.currentTimeMillis()
        println("开始执行:$time")
        // 返回 A 协程的执行结束时间
        time
    }
    // (2)启动协程 B
    val deferredB = async {
        println("开始执行协程B")
        delay(1000)
        val time = System.currentTimeMillis()
        println("开始执行2:$time")
        // 返回 B 协程的执行结束时间
        time
    }
    // (3)A协程 await
    val timeA = deferredA.await()
    val timeB = deferredB.await()

    println("协程 A与B的时间间隔:${timeA - timeB}")
}
输出结果:
开始执行协程A
开始执行协程B
开始执行2:1577772890101
开始执行:1577772891102
协程 A与B的时间间隔:1001

在这里插入图片描述
从上面代码我们容易进入一个误区,大伙有没有感觉应该是当我调用 A 协程的 await 的时候,不应该是 A 协程执行完返回结果,在调用 B 协程的 await 才开始执行 B 协程的吗?

一定要分清楚这个哦?不是这样的流程,因为没有改变 start 参数,默认时使用

CoroutineStart.DEFAULT 的时候,若我们改变 start 参数为 CoroutineStart.LAZY 才会是想象中的过程。

其实上面的例子中, B 协程并不是调用 deferredB.await() 的时候才会执行的,如果 B 协程在 await 之前就执行完毕,当调用 await 的时候就会直接返回值。

官方是这样描述的:

使用 .await() 在一个延期的值上得到它的最终结果。但是 Deferred 也是一个 Job,所以如果需要的话,你可以取消它。 async 可以通过将 start 参数设置为 CoroutineStart.LAZY 而变为惰性的。 在这个模式下,只有结果通过 await 获取的时候协程才会启动。
在这里插入图片描述
好吧,我尽力文字去描述了,我读书少只能描述到这个程度了,我们还是看图吧!
在这里插入图片描述
呀!不好意思放错了!!!
在这里插入图片描述上面代码系列过程中,我用颜色标注了容易出错的过程。

  1. B 协程并没有在调用其 await 才执行,它在 A 协程执行挂起函数的时候就开始执行了。
  2. A 协程虽然先执行,但是不代表 A 一定比 B 结束的早,这还要看谁的 suspend 挂起函数先执行完,当 A 执行挂起函数的时候(挂起函数在其他线程执行哦!前面讲过哦),此时 B 协程就开始执行啦,这就是传说的非阻塞挂起。
  3. 当调用 deferredB.await() 的时候,B 早就执行完毕啦,直接就会返回结果。

总结&实战

终于讲解完了,我尽力的讲清楚了,大家先不要在乎其他的知识点,只需要把这个协程启动流程搞明白,什么 CoroutineContext 丶 CoroutineScope 丶 Dispatchers 大家可以先不用关注,揉在一起你很难理解。

接下来我们来个常用的实战技巧。
在这里插入图片描述

题目:
并行异步读取 A B 2个文件内容,当 A 和 B 文件内容同时读取完后合并。

注意题目是 并行异步 哦,要 2 个线程同时读取文件,2 个线程都执行完毕后才输出哦!

fun main() = runBlocking<Unit> {
    // 启动协程 读取文件 A ,用 launch 呢?还是用 async呢?
    val deferredA = async {
        readFile(File("/Users/wenyingzhi/Desktop/project/KotlinDemo/build.gradle"))
    }
    val deferredB = async {
        readFile(File("/Users/wenyingzhi/Desktop/project/KotlinDemo/gradle.properties"))
    }
    // 等待 A 和 B 协程都执行完毕
    val dataA = deferredA.await()
    val dataB = deferredB.await()
    println("A 和 B 协程执行完毕,数据:$dataA  $dataB")
}

/**
 * 定义 异步读取文件 挂起函数
 */
suspend fun readFile(file: File): String {
    /**
     * 切记要其他线程执行哦
     * 还记得 挂起函数篇章中 讲解的怎么创建线程的吗?
     */
    return withContext(Dispatchers.IO) {
        file.readText()
    }
}

是不是同步代码写出功能啦?这就是协程出现的目的,解决地狱回调。

这里大家应该不理解 Dispatchers 是在干嘛?我们先不管,反正你现在理解就是创建了个线程在读取文件就行。至于 withContext 我们在挂起函数篇章中讲过。

当然我们也可以自己创建线程,只不过挂起函数要调用 suspendCoroutine 方法。

挂起函数定义可以改写成这样:

suspend fun readFile(file: File): String = suspendCoroutine<String>{
      // 创建一个线程
    thread {
        val txt = file.readText()
        /**
         * 忘记了的话,自行去看我的挂起函数篇章
         *  还记的 resume 吗?
         */
        it.resume(txt)
    }
}

补充下 A 与 B 的 2 个 await 方法一定要在,创建协程之后允调用哦,可不能在中间调用哦。至于为啥?我认为大家读明白了文章应该能理解。

例如:下方代码有问题吗?

fun main() = runBlocking<Unit> {
    // 启动协程 读取文件 A ,用 launch 呢?还是用 async呢?
    val deferredA = async {
        readFile(File("/Users/wenyingzhi/Desktop/project/KotlinDemo/build.gradle"))
    }

    /**
     * 错误的写法,注意这里:
     * 错误的将 deferredA.await() 放在这里
     */
    val dataA = deferredA.await()

    val deferredB = async {
        readFile(File("/Users/wenyingzhi/Desktop/project/KotlinDemo/gradle.properties"))
    }

    val dataB = deferredB.await()
    println("A 和 B 协程执行完毕,数据:$dataA  $dataB")
}

/**
 * 定义 异步读取文件 挂起函数
 */
suspend fun readFile(file: File): String = suspendCoroutine<String>{
    thread {
        val txt = file.readText()
        /**
         * 忘记了的话,自行去看我的挂起函数篇章
         *  还记的 resume 吗?
         */
        it.resume(txt)
    }
}

以上是关于4.协程-启动&分析执行过程的主要内容,如果未能解决你的问题,请参考以下文章

kotlin协程硬核解读(4. 协程的创建和启动流程分析)

kotlin协程硬核解读(4. 协程的创建和启动流程分析)

kotlin协程硬核解读(4. 协程的创建和启动流程分析)

进程&线程&协程

Kotlin协程源码分析-协程的启动

进程和线程和协程之间的关系