【深入kotlin】 - 协程上下文、Job和CoroutineScope

本文深入探讨了Kotlin协程的调试技巧,包括如何通过`kotlinx.coroutines.debug`参数查看协程名。文章还讲解了线程上下文的概念,展示了`newSingleThreadContext`的使用,并分析了Job和Deferred的区别。同时,介绍了父子协程的关系,以及GlobalScope在协程管理中的特殊性。最后,提到了CoroutineName用于协程命名,并讨论了CoroutineScope的生命周期及其对协程取消的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

协程调试
打印协程名

首先看这段代码:

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 开发中可用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值