End

Kotlin 朱涛-14 协程 启动 调试 launch async

本文地址


目录

14 | 如何启动协程?

// 确保添加了协程的依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'

// 后面的代码均省略了协程、线程相关的导包代码
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.concurrent.thread

如何调试协程

可以通过两种方式调试协程:配置 VM 参数、断点调试。

配置 VM 参数

只需配置特殊的 VM 参数:-Dkotlinx.coroutines.debug

配置后,如果当前代码是运行在协程中的,Thread.currentThread().name 就会包含协程的名字 @coroutine#1

断点调试

  • 将 IntelliJ 升级到最新版本
  • 确保 IDE 自带的 Kotlin 编译器插件版本号大于 1.4
    • File | Settings | Languages & Frameworks | Kotlin
  • 为协程代码打断点,确保勾选 suspendAll
  • 当程序停留到断点处以后,确保协程调试窗口(Coroutines)被打开了

在协程调试窗口中,我们可以看到很多有用的协程信息,包括:

  • 当前协程的名字:coroutine#1
  • 当前协程运行在哪个线程之上:DefaultDispatcher-worker-1
  • 当前协程的运行状态:RUNNING
  • 当前协程的 创建调用栈

launch 启动协程

一个守护线程案例

为了更好的理解下面的代码,我们先看一个守护线程的案例。

fun main() {
    thread(isDaemon = true) { // 守护线程
        Thread.sleep(100L)    // 休眠一段时间,确保主线程已经结束了
        println("bqt")        // 不会打印任何内容
    }
}

以上代码不会打印任何内容。因为我们创建的 Thread 其实是一个守护线程。守护线程就意味着,当主线程结束的时候,它也会跟着被销毁。这就导致 println 这行代码没有机会执行。

launch 是非阻塞的

fun main() {
    GlobalScope.launch {    // ① 启动一个协程。生产环境不建议使用 GlobalScope
        println("start")    // ②
        delay(100L)         // ③ 非阻塞延迟
        println("end")      // ④
    }

    println("after launch") // ⑤
    Thread.sleep(120L)      // ⑥ 让当前线程休眠,目的是不让主线程这么快退出
    println("process exit") // ⑦
}
after launch
start
end
process exit
  • GlobalScope.launch{} 是一个高阶函数,它的作用就是启动一个协程
    • GlobalScope 是 Kotlin 官方提供的协程作用域。这涉及到协程的结构化并发理念,会在后面第 16、17 讲里解释
  • delay() 的作用就是延迟,他是非阻塞
    • 方法签名:public suspend fun delay(timeMillis: Long)
    • 函数签名中有一个suspend关键字,代表是一个挂起函数,也就意味着,拥有挂起和恢复的能力
    • 既然拥有挂起和恢复的能力,那么肯定能实现非阻塞。关于挂起函数的更多知识点,会在后面第 15 讲里介绍
  • Thread.sleep() 的作用就是让当前线程休眠,目的是不让主线程这么快退出
    • 如果删掉此行代码,协程代码就无法正常工作了,因为通过 launch 创建的协程还没来得及开始执行,整个程序就已经结束
    • 如果删掉此行代码,只会打印 ⑤⑦

以上协程代码的执行顺序是:1、5、6、7、2、3、4。

可以看到,launch 并不会阻塞线程的执行。甚至,我们可以认为 launch() 当中 Lambda 一定就是在函数调用之后才执行的。当然,在特殊情况下,这种行为模式也是可以打破的,这一点会在第 17 讲中详细探讨。

launch 拿不到执行结果

上面我们通过 launch 启动一个协程以后,并没有让协程为我们返回一个执行结果,这其实就是典型的 Fire-and-forget 的应用场景。

launch 一个协程任务,就像猎人射箭一样:

  • 箭一旦射出去了,目标就无法再被改变;协程一旦被 launch,那么它当中执行的任务也不会被中途改变
  • 箭如果命中了猎物,猎物也不会自动送到我们手上来;launch 的协程任务一旦完成了,即使有了结果,也没办法直接返回给调用方

launch 之所以无法将结果返回给调用方,是因为这个函数的返回值是一个 Job,它代表的是协程的句柄(Handle),它并不能为我们返回协程的执行结果。

launch 的函数声明分析

public fun CoroutineScope.launch(                      // 扩展函数
    context: CoroutineContext = EmptyCoroutineContext, // 上下文
    start: CoroutineStart = CoroutineStart.DEFAULT,    // 启动模式
    block: suspend CoroutineScope.() -> Unit           // 函数类型
): Job { ... }
  • 首先是 CoroutineScope.launch(),代表了 launch 其实是一个扩展函数,它的扩展接收者类型是 CoroutineScope。前面我们使用的 GlobalScope,就是官方提供的一个 CoroutineScope 对象。

  • 第一个参数 CoroutineContext,代表了协程的上下文,它的默认值是 EmptyCoroutineContext。也可以传入官方提供的 Dispatchers 来指定协程运行的线程池。协程上下文是协程中非常关键的元素,具体会在第 17 节中探讨。

  • 第二个参数 CoroutineStart,代表了协程的启动模式,它的默认值是 CoroutineStart.DEFAULT。这个枚举类的枚举值有:DEFAULT、LAZY、ATOMIC、UNDISPATCHED。最常用的是 DEFAULT (立即执行) 和 LAZY (懒加载执行)。

  • 最后一个参数 block,代表一个函数类型

    • () -> Unit 代表无参数、无返回的函数
    • suspend () -> Unit 代表无参数、无返回的挂起函数
    • suspend X.() -> Unit 代表这个函数是 X 类的成员方法或是扩展方法
fun func1(num: Int): Double = num.toDouble() // 函数类型是 (Int) -> Double
val f1: (Int) -> Double = ::func1            // 函数引用

fun func2(): Unit = println("func2")         // 函数类型是 () -> Unit
val f2: () -> Unit = ::func2                 // 函数引用

suspend fun func3(): Unit = println("func3") // 是一个挂起函数
val f3: suspend () -> Unit = ::func3         // 带 suspend 的函数引用

suspend fun Int.fun4(): Unit = println("f4") // 是一个带接收者类型的挂起函数
val f4: suspend Int.() -> Unit = Int::fun4   // 带接收者类型的函数引用

runBlocking 启动协程

runBlocking 可以从协程中返回执行结果,但由于它是阻塞式的,因此,并不适用于实际的工作当中。

会阻塞当前线程的执行

将前面案例中的 launch 的代码改为 runBlocking

fun main() {
    runBlocking {           // ① 启动一个协程。仅改了这一行代码
        println("start")    // ②
        delay(100L)         // ③ 非阻塞延迟
        println("end")      // ④
    }

    println("after launch") // ⑤
    Thread.sleep(200L)      // ⑥ 让当前线程休眠,目的是不让主线程这么快退出
    println("process exit") // ⑦
}
start
end
after launch
process exit

可以发现,使用 runBlocking 启动的协程会阻塞当前线程的执行。此时,代码就变成了顺序执行:1、2、3、4、5、6、7。

不要在生产环境中使用

fun main() {
    runBlocking {
        print("1 - ${Thread.currentThread().name} - ")
        delay(100L)
        println("Hello First!")
    }
    runBlocking {
        print("2 - ${Thread.currentThread().name} - ")
        delay(100L)
        println("Hello Second!")
    }
    runBlocking {
        print("3 - ${Thread.currentThread().name} - ")
        delay(100L)
        println("Hello Third!")
    }
    // 删掉了 Thread.sleep()
    println("Process end!")
}
1 - main @coroutine#1 - Hello First!
2 - main @coroutine#2 - Hello Second!
3 - main @coroutine#3 - Hello Third!
Process end!
  • 上面我们调用了三次 runBlocking,所以程序启动了三个协程
  • 上面我们删掉了末尾的 Thread.sleep(),而程序仍然按顺序执行了,这进一步说明,runBlocking 会阻塞当前线程的执行

Kotlin 官方强调:runBlocking 只推荐用于连接线程与协程,并且,大部分情况下,都只应该用于编写 Demo 或是测试代码。

所以,请不要在生产环境中使用 runBlocking

可以返回执行结果

runBlocking 的函数声明如下:

public actual fun <T> runBlocking( // 顶层函数,而非 CoroutineScope 的扩展函数
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T): T { ... } // 有返回值
  • runBlocking 是一个普通的顶层函数,而不是 CoroutineScope 的扩展函数,因此,调用时不需要 CoroutineScope 的对象
  • 第二个参数 suspend CoroutineScope.() -> T 是有返回值的函数类型
    • 且它的返回值类型跟 runBlocking 的返回值类型是一样的
    • 因此,runBlocking 是可以从协程中返回执行结果的
fun main() {
    val result = runBlocking {    // 接收返回值
        delay(100L)
        return@runBlocking "bqt"  // return@runBlocking 可省略
    }
    println("Result is: $result") // Result is: bqt
}

async 启动协程

使用 async 启动协程以后,它不会阻塞当前程序的执行,且能通过它返回的句柄拿到协程的执行结果

可以拿到协程的执行结果

suspend fun main() {
    val deferred: Deferred<String> = GlobalScope.async {
        println("In async - ${Thread.currentThread().name}")
        delay(100L)        // 模拟耗时操作
        return@async "bqt" // 返回执行结果
    }

    val name = Thread.currentThread().name
    println("After async1 - $name")  // 注意他的执行顺序
    Thread.sleep(110L)               // 模拟耗时操作
    println("After async2 - $name")  // 注意他的执行顺序
    val result = deferred.await()    // 获取执行结果
    println("Result is: $result")
}
After async1 - main
In async - DefaultDispatcher-worker-1 @coroutine#1
After async2 - main
Result is: bqt
  • async 启动协程以后,不会阻塞当前程序的执行流程,因为 After async1In async 的前面就已经输出了
  • async 的返回值是一个 Deferred 对象,通过其 await() 方法可以拿到协程的执行结果
  • 注意:即使不调用 await() 方法,async 中的代码也会执行,因为 In asyncawait() 执行前就已经输出了

async 和 launch 的区别

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit // 函数类型不同
): Job {}                                    // 返回值类型不同

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T    // 函数类型不同
): Deferred<T> {}                            // 返回值类型不同

launch 和 async 的不同点

  • block 的函数类型不同:前者的返回值类型是 Unit,后者则是泛型 T
  • 函数的返回值类型不同:前者返回值类型是 Job,后者则是 Deferred

小结

三种启动协程的方式:

  • launch,是典型的Fire-and-forget场景,它不会阻塞当前程序的执行流程,但是我们无法获取协程的执行结果。它有点像是生活中的射箭。
  • runBlocking,我们可以获取协程的执行结果,但会阻塞代码的执行流程,它一般用于测试用途,生产环境中不推荐使用。
  • async,是很多编程语言当中普遍存在的协程模式。它既不会阻塞当前的执行流程,还可以直接获取协程的执行结果。它有点像是生活中的钓鱼。

2016-04-26

posted @ 2016-04-26 13:39  白乾涛  阅读(7821)  评论(0编辑  收藏  举报