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
- 为协程代码打断点,确保勾选
suspend
和All
- 当程序停留到断点处以后,确保协程调试窗口(
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 讲里解释
- GlobalScope 是 Kotlin 官方提供的
- ③
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 async1
在In async
的前面就已经输出了 - async 的返回值是一个
Deferred
对象,通过其await()
方法可以拿到协程的执行结果 - 注意:即使不调用
await()
方法,async 中的代码也会执行,因为In async
在await()
执行前就已经输出了
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
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/5434774.html