协程调试
打印协程名
首先看这段代码:
private fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main() = runBlocking<Unit> {
val a = async {
log("a")
10
}
val b = async {
log("b")
20
}
log(" a*b = ${a.awiat()*b.await()}")
}
输出:
[main] a
[main] b
[main] a*b = 200
async 如果不指定任何协程分发器的话,就等于 launch(使用外层协程的线程)。从线程的角度看,协程 a 和 b 都是在同一个线程中,异步不异步我们看不出任何区别。但从协程的角度看,协程 a 和 b 是异步进行的。因此,光是打印线程信息并不能满足我们的需要,我们需要了解更底层的协程信息。kotlin 通过修改 Run/Debug Configurations 的 VM options 为 -Dkotlinx.coroutines.debug 来实现这一点:
[main @coroutine#2] a
[main @coroutine#3] b
[main @coroutine#1] a*b = 200
@coroutine#* 就表示协程名。其中 @coroutine#1 就是最外层的 runBlocking 协程,@coroutine#2 是 a 协程,@coroutine#3 是 b 协程,在 main 函数中总共创建了 3 个协程。
线程上下文
线程上下文也是一种协程分发器。
fun main() {
newSingleThreadContext("Context1").use{ // newSingleThreadContext 是一种单线程分发器, use 方法执行指定块,无论是否异常,资源都会被关闭
ctx1 ->
newSingleThreadContext("Context2").use {
ctx2 ->
runBlocking(ctx1){
log("Started in context1")
withContext(ctx2){// 用 ctx2 执行
log("Working in context2")
}
log("Back to context1")
}
}
}
}
输出:
[Context1] Started in context1
[Context2] Working in context2
[Context1] Back to context1
newSingleThreadContext 是一种单线程分发器,参数中的指定的是线程的名字。use 方法在指定线程中执行指定块(协程)并自动释放线程。现在打开 VM options 中的 coroutine.debug 参数,再次打印出协程信息:
[Context1 @coroutine#1] Started in context1
[Context2 @coroutine#1] Working in context2
[Context1 @coroutine#1] Back to context1
但是我们发现,这里打印出了两个协程。
- 一个是 [Context1 @coroutine#1] ,这个协程是 runBlocking 所创建的。
- 一个是[Context2 @coroutine#1] ,这个协程是 withContext 所创建的。
Job 和 Deferred
Job 很好理解,就是指协程需要执行的代码,但它没有返回值。Deferred 是 Job 的子类,但是与 Job 不同,它拥有返回值。
Job
协程上下文中包含了 job。通过协程上下文可以获取协程的 Job。此外,launch 方法也会返回 job 对象。但是 runBlocking 是个例外,它不会返回 job,对于这种情况,Kotlin 提供了一种便利的方法:
fun main() = runBlocking<Unit> {
val job: Job? = coroutineContext[Job] // coroutineContext 是 CoroutineScope 的属性,代表当前协程的上下文
println(job) // 打印:BlockingCoroutine(Active)@4ccabbaa
println(coroutineContext.isActive) // 打印 true
println(coroutineContext[Job]?.isActive) // 和上面等价
}
BlockingCoroutine(Active) 表示当前协程状态。如果打开 coroutines.debug VM 选项,则打印出协程名:
"coroutine#1":BlockingCoroutine(Active)@5b90330d
coroutineContext[job] 在 kotlin 中称作“表达式”。CoroutineContext 是一个 [Element] 集合。这种集合中的元素具有唯一 key,因此可以通过 key 来访问其元素。
BlockingCoroutine 是一个 AbstractCoroutine 类,代表了一个协程的具体实现类。它同时实现了 Job 和 CoroutineScope 接口(还有 Continuation 接口),因此你也可以把它当 job 看待。因此当你打印 job 时,发现它当真实类型其实是 BlockingCoroutine。
此外,Job 接口有一个 isActive 的 Boolean,代表了协程的活动状态。当它为 true 时表示协程是“活动的”。协程上下文的状态包括:“活动的”、“已取消”,“已完成”。
父子协程
当在一个协程中启动另一个协程时,这就形成了父子协程的关系,子协程会会继承父协程的上下文(通过 CoroutineScope.coroutineContext)。
但是 GlobalScope 是个例外,它是一种特殊的 CoroutineScope,它不会绑定任何 job。我们经常用它来启动“顶层”的协程,如果使用 GlobalScope 来启动协程,顶层协程运行在整个程序的生命周期,不会提前取消。
GlobalScope 是一个 object 类,因此它是单例。
fun main() = runBlocking<Unit> {
val request = lanuch{
GlobalScope.launch{
println("global scope: before delay 1 sec")
delay(1000)
println("global scope: after delay 1 sec")
}
lanuch {
delay(100)
println("launch: before delay 1 sec")
delay(1000)
println("launch: before delay 1 sec")
}
}
delay(500)
request.cancel()
delay(1000)
println("end...")
}
输出结果:
global scope: before delay 1 sec
launch: before delay 1 sec
global scope: after delay 1 sec
end...
println(“launch: before delay 1 sec”) 一句没有执行,因为 500 毫秒时父协程被 cancel 了。但是 println(“global scope: after delay 1 sec”) 却执行了,因为它的协程是 GlobalScope 所启动的,是一个“顶层协程”,不会被提前取消,它的生命周期是这个程序的生命周期。
当父协程 request 取消时,它的所有子协程也会被取消,但 GlobalScope 启动的协程是个例外,它们是”顶层协程“,不可能有任何父协程,因此虽然它是在 request 内部启动的,但它仍然不可能作为 request 的子协程。
父协程总是会等待其所有子协程完成。父协程不必显式地追踪所有子协程,也不必用 Job.join() 方法来等待子协程完成。
fun main()=runBlocking<Unit> {
val request = launch {
repeat(5) { i ->
launch {
delay((i+1) * 100L)
println("Coroutine ${i+1} done.")
}
}
println("Coroutine 0 done.")
}
request.join() // 挂起协程,直至 job 完成,这样父协程会等 request 执行完才继续下一句
println("Done.")
}
输出结果:
Coroutine 0 done.
Coroutine 1 done.
Coroutine 2 done.
Coroutine 3 done.
Coroutine 4 done.
Coroutine 5 done.
Done.
从此可以看到,request 内部的子协程并没有显示 join,但最终仍然会等待 5 个子协程都完成 request 才返回。
CoroutineName
CoroutineName 类用于协程的命名:
private fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main() = runBlocking(CoroutineName("main")){
log("----")
val job1 = async(CoroutineName("coroutine1")){
delay(800)
log("coroutine1 log")
30
}
val job2 = async(CoroutineName("coroutine2")){
delay(1000)
log("coroutine2 log")
5
}
log("a * b = ${job1.await() * job2.await()}")
}
CoroutineName 是一个数据类,其 name 属性用于指定协程名,它继承了 AbastractCoroutineContextElement -> Element -> CoroutineContext。因此也可以把它当成 CoroutineContext 传入 runBlocking 的参数。
打开 coroutines.debug VM 参数,运行程序:
[main @main#1] ----
[main @coroutine1#2] coroutine1 log
[main @coroutine2#3] coroutine2 log
[main @main#1] a * b = 150
注意,这里的输出语句使用了 log 而不是 println,所以在打印内容中自动加上了线程和协程信息,比如 [main @main#1]。
CoroutineContext 的 + 运算
如果想在启动协程时传入多个上下文,比如一个 CoroutineName 用于指定协程名,一个CoroutineDispatcher 用于指定分发器,那么可以利用 CoroutineContext 的运算符重载功能对二者进行 + 运算:
fun main() = runBlocking<Unit>{
launch(Dispatchers.Default+CoroutineName("job1")){
println("thread: ${Thread.currentThread().name}")
}
}
输出结果(加上 coroutines.debug 参数):
thread: DefaultDispatcher-worker-1 @job1#2
CoroutineScope 生命周期
CoroutineScope 拥有自己的生命周期,当一个 CoroutineScope 被取消时,位于该 CoroutineScope 下面的所有协程将自动被取消。
class Activity: CoroutineScope by CoroutineScope(Dispactcher.Default){
fun destory() {
println("销毁 Activity")
cancel() // 调用委托的 cancel 方法
}
fun doSomething(){
repeat(8) { i->
launch {
delay((i+1)*300L)
println("Coroutine $i is finished.")
}
}
}
}
fun main() = runBlocking<Unit> {
val activity = Activity()
activity.doSomething()
println("启动协程")
delay(1300L)
activity.destroy()
delay(5000L)
}
代码中 CoroutineScope(context:) 是一个工厂函数,它创建了一个 CoroutineScope。
MainScope() 是另一个工厂函数,创建一个运行于主 UI 线程的 CoroutineScope。它的上下文中包括了 SupervisorJob 和 Dispatchers.Main。Dispatchers.Main 分发器将协程绑定到主线程。
doSomething 本应该创建 8 个协程,但因为 Activity 在执行到 1300 毫秒的时候销毁,导致 8 个协程中只有前 4 个协程有机会执行,其它的直接被取消了。输出结果如下:
启动协程
Coroutine 0 is finished.
Coroutine 1 is finished.
Coroutine 2 is finished.
Coroutine 3 is finished.
销毁 Activity
如果将 Dispactcher.Default 改成 Dispactcher.Main,运行程序,报错:
Exception in thread "main" java.lang.IllegalStateException: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android' ...
这说明 main 分发器并没有包含在 kotlinx-coroutine-core 包中,而是在 kotlinx-coroutines-android 中。所以实际上 main 分发器是专门用于 UI 开发的,仅在 android 开发中可用。