浅谈Coroutine

267 阅读5分钟

一.Coroutine是什么?

协程(Coroutine)是一种编程概念,用于简化异步编程和非阻塞操作。它们可以被看作是轻量级的线程,但与线程相比,协程提供了更高的执行效率和更低的资源消耗。

举个参考资料1中的例子,假设如今有2个函数,functionA()和functionB(),其源码如下所示。

fun functionA(case: Int) {
    when (case) {
        1 -> {
            println("Execute functionA mission 1")
            functionB(1)
        }
        2 -> {
            println("Execute functionA mission 2")
            functionB(2)
        }
        3 -> {
            println("Execute functionA mission 3")
            functionB(3)
        }
        4 -> {
            println("Execute functionA mission 4")
            functionB(4)
        }
    }
}
fun functionB(case: Int) {
    when (case) {
        1 -> {
            println("Execute functionB mission 1")
            functionA(2)
        }
        2 -> {
            println("Execute functionB mission 2")
            functionA(3)
        }
        3 -> {
            println("Execute functionB mission 3")
            functionA(4)
        }
        4 -> {
            println("Execute functionB mission 3")
        }
    }
}

在main()方法调用functionA(),并传入参数1。

fun main(){
    functionA(1)
}

运行一下。

可以看见线程在functionA()和functionB()之间反复横跳。

回到Coroutine。

假设functionA()在等待IO事件响应,这时functionA()的状态会被保存下来,底层线程被释放去执行functionB(),等到IO事件响应了,这时functionA()的状态会被恢复,底层线程继续执行functionA(),这正是Coroutine做的事情。

  • Coroutine存在的意义是什么?

    • 避免线程的挂起,提高线程的利用率。比如说线程在等待IO事件响应的时候,与其将其挂起,不如让线程去执行其它函数。
    • 避免回调地狱。

    协程不是来替代线程的,相反,协程是一套并发/异步编程框架,让线程更好用的。

二.suspend 关键字的工作原理

Kotlin协程的suspend关键字用于声明挂起函数,这类函数可以在不阻塞线程的情况下暂停执行,并在异步操作完成后恢复。 当kotlin编译器遇到 suspend 关键字时,它会将

  • 挂起函数转换为一个带额外参数的普通函数:Continuation(可以理解为Callback),这个参数用于管理函数在挂起和恢复时的状态。
  • 将函数的返回值变为Any?。

上述这个过程称为CPS(Continuation-Passing Style)转换。

举个🌰:

import kotlinx.coroutines.*

suspend fun fetchData(): String {
    delay(1000L) // 模拟网络请求
    return "Data loaded"
}

fun main() = runBlocking { // 创建协程作用域
    println("Start")
    val result = fetchData() // 挂起函数调用(不会阻塞线程)
    println(result)
    println("End")
}

上述代码编译后的等效代码如下:

// 
// 返回值变为Any?,增加Continuation参数
fun main() {
    // 创建最终结果回调的Continuation
    val continuation1 = object : Continuation<String> {
        override val context = EmptyCoroutineContext
        override fun resumeWith(result: Result<String>) {
            println("最终结果: ${result.getOrNull()}")
        }
    }

    // 启动协程(模拟launch)
    val result = fetchData(continuation1)
    
    ...
}

// 编译器转换后的fetchData实现
fun fetchData(continuation: Continuation<*>): Any? {
    class StateMachine(
        private val completion: Continuation<String>
    ) : Continuation<Unit> {
        var label = 0
        
        override val context = completion.context
        
        override fun resumeWith(result: Result<Unit>) {
            when (label) {
                0 -> {
                    label = 1
                    if (delay(1000L, this) == COROUTINE_SUSPENDED) return
                }
                1 -> result.getOrThrow() // 检查异常
            }
            completion.resumeWith(Result.success("Data loaded"))
        }
    }

    val continuation2 = continuation as? StateMachine ?: StateMachine(continuation as Continuation<String>)
    
    return when (continuation2.label) {
        0 -> {
            continuation2.label = 1
            val suspendResult = delay(1000L, sm)
            if (suspendResult == COROUTINE_SUSPENDED) COROUTINE_SUSPENDED else Unit
        }
        1 -> {...}
    }
}

// 模拟delay实现
fun delay(timeMs: Long, cont: Continuation<*>): Any? {
    thread(isDaemon = true) {
        Thread.sleep(timeMs)
        cont.resumeWith(Result.success(Unit))
    }
    return COROUTINE_SUSPENDED
}

具体的流程图如下所示。

sequenceDiagram
    participant main as main()
    participant cont1 as continuation1
    participant fetchData as fetchData()
    participant cont2 as continuation2
    participant delay as delay()
    participant Dispatcher as Dispatchers

    main->>cont1:实例化
    main->>+fetchData: 调用fetchData(continuation1)
    fetchData->>cont2:实例化,包裹continuation1
    fetchData->>+delay: delay(1000L, continuation2)
    delay->>Dispatcher: 提交IO线程任务
    delay-->>fetchData: 返回COROUTINE_SUSPENDED
    fetchData-->>main: 返回COROUTINE_SUSPENDED
    deactivate fetchData

    Note over Dispatcher: 1秒后异步完成
    Dispatcher-->>delay: 确认调度
    delay->>cont2: (Result.success(Unit))
    activate cont2
    cont2->>+fetchData: 恢复执行(label=1)
    fetchData->>cont1: resumeWith(Result.success("Data loaded"))
    activate cont1
    cont1-->>main: 回调结果
    deactivate cont1
    deactivate fetchData

Continuation是一个接口,代码如下,可以看到Continuation和Callback的定义是类似的。

@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

三.CoroutineScope

CoroutineScope 是 Kotlin 协程中的一个基本概念,它定义了协程的作用域:

  • 所有的协程都必须在协程作用域 (CoroutineScope) 内启动,
  • 当协程作用域结束或取消时,其所有子协程都会取消,此即为结构化并发。

解决了什么问题

  • 生命周期管理。

    举个Android开发中的例子。

    当Activity在onDestroy()回调时,为了避免资源泄漏,不需要逐一关闭其发起过的所有协程时,只需要取消掉协程作用域即可。

代码

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

在代码层面中,CoroutineScope是一个接口,只有一个字段coroutineContext,即协程上下文。

public interface CoroutineContext {
    
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...

    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        
    }
}

CoroutineContext是一个集合接口,存储CoroutineContext.Element元素。

实现CoroutineContext.Element接口中比较重要的有Job,Dispatchers,CoroutineName,ExceptionHandler类。

Job

Job用于管理协程的生命周期,每次启动协程都会返回一个Job。

Job总共有六个状态,分别是NewActiveCompletingCompletedCancellingCancelled,尽管不能直接获取这六个状态,可以通过访问Job的isActive,isCancelled和 isCompleted字段来判断协程的状态。

fun main() {
    val coroutineContext=Dispatchers.Default+CoroutineName("JobDemo")
    val job=CoroutineScope(coroutineContext).launch {
        //延迟0.1秒
        delay(100)
    }
    while(job.isActive){
        println("The coroutine is still active ")
    }
    if (job.isCompleted){
        println("The coroutine is completed")
    }
}

运行一下。

Job里有一个cancel() 方法,该方法作用是取消掉协程。

Dispatchers

Dispatchers可以指定协程执行任务所在的线程,Dispatchers有三种类型。

  • Main。

    应用该Dispatcher的协程会在主线程执行,一般用于UI相关操作。

    GlobalScope.launch(Dispatchers.Main) {
           updateUI()
       }
    
  • Default。

    应用该Dispatcher的协程适合执行CPU密集型的任务。

    GlobalScope.launch(Dispatchers.Default) {
          someCpuIntensiveWork()
       }
    
  • IO。

    应用该Dispatcher的协程适合执行IO任务。

    GlobalScope.launch(Dispatchers.IO) {
          fetchDataFromServer()
       }
    

CoroutineName

CoroutineName是协程名字。

CoroutineExceptionHandler

CoroutineExceptionHandler是协程异常处理器,当协程抛出异常,就会调用CoroutineExceptionHandler的handleException()方法,需要注意的是,CoroutineExceptionHandler只会在顶级协程生效。

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    
    public fun handleException(context: CoroutineContext, exception: Throwable)
}

来个小demo。

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default+CoroutineName("ExceptionHandlerDemo")+exceptionHandler
    CoroutineScope(coroutineContext).launch (){
        throw Exception()
    }
    //CoroutineScope(context)为顶级协程,线程休眠0.1秒等待协程执行完成。
    Thread.sleep(100)
}

运行一下。

三.异常处理

try-catch块

除了CoroutineExceptionHandler之外,异常还可以用常规的try-catch块处理。

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default+CoroutineName("ExceptionHandlerDemo")+exceptionHandler
    CoroutineScope(coroutineContext).launch (){
        try {
            throw Exception()
        }catch (exception:Exception){
            println("Caught exception in try-catch block")
        }    
    }
    //CoroutineScope(context)为顶级协程,线程休眠0.1秒等待协程执行完成。
    Thread.sleep(100)
}

运行一下。

上面的代码既有CoroutineExceptionHandler又有try-catch块,而抛出的异常被try-catch块给处理了,换句话说,CoroutineExceptionHandler更多是作为一个兜底措施,只有在协程内没有进行异常处理,才会触发CoroutineExceptionHandler。

子协程的异常处理

顶层协程的CoroutineExceptionHandler

如果是子协程的异常,且子协程没有进行任何异常处理,会出现什么情况?

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default+CoroutineName("ExceptionHandlerDemo")+exceptionHandler
    CoroutineScope(coroutineContext).launch (){//父协程
        try {
            launch {//发起子协程
                throw Exception()
            }
        }catch (exception:Exception){
            println("Caught exception in try-catch block")
        }
    }
    //CoroutineScope(context)为顶级协程,线程休眠0.1秒等待协程执行完成。
    Thread.sleep(100)
}

运行一下。

可以看到,CoroutineExceptionHandler被调用了,这是因为当子协程没有进行任何异常处理的时候,会将异常向上传递给顶层协程(没有父协程的协程)的CoroutineExceptionHandler,如下图所示。

图片来源见参考资料4

上图可以通过一个简单的例子来验证一下。

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("Top-Level CoroutineExceptionHandler got $exception")
    }
    val subExceptionHandler= CoroutineExceptionHandler{_,exception->
        println("SubCoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")
    CoroutineScope(coroutineContext+exceptionHandler).launch() {//顶层协程
        try {
            launch (subExceptionHandler){//第二层协程
                launch(subExceptionHandler){//第三层协程
                    launch (subExceptionHandler){//第四层协程
                        throw Exception()
                    }
                }
            }
​
        } catch (exception: Exception) {
            println("Caught $exception in try-catch block")
        }
    }
    Thread.sleep(100)
}

上面代码中每一个子协程都有一个subExceptionHandler,只有顶层协程是exceptionHandler,异常会在向上传递过程中被subExceptionHandler处理吗?运行一下。

可以看到,异常只传递给了顶层协程的CoroutineExceptionHandler。

那么用try-catch块把没有CoroutineExceptionHandler的顶层协程给包起来能处理异常吗。

fun main() {
    val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")//没有CoroutineExceptionHandler
    try {//用try-catch块把顶层协程给包起来
        CoroutineScope(coroutineContext).launch() {
            try {
                launch {
                    throw Exception()
                }
            } catch (exception: Exception) {
                println("Caught $exception in try-catch block")
            }
        }
    }catch (exception: Exception){
        println("Caught $exception in try-catch block")
    }
}

答案是不能。

image-20220912163138651

原因是协程运行在的线程是在DefaultDispatcher-worker-3线程,main线程的try-catch块无法处理DefaultDispatcher-worker-3线程的异常,对于这点有疑问的小伙伴可以自行去查阅一下子线程的异常处理方式,这里就不再做演示了。

因此,为每一个顶层协程都设置一个CoroutineExceptionHandler来作为兜底措施是一个良好的编程规范。

coroutineScope和supervisorScope

那有没有一种可以用try-catch块的方式来处理子协程的异常?有的,用coroutineScope将子协程包起来就可以了。

fun main() {
​
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("Top-Level CoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")+exceptionHandler
    CoroutineScope(coroutineContext).launch() {
        try {
            coroutineScope {
                launch {
                    throw Exception()
                }
            }
        } catch (exception: Exception) {
            println("Caught $exception in try-catch block")
        }
    }
    Thread.sleep(100)
}

运行一下。

提到coroutineScope,就不得提一提supervisorScope了,二者区别是

  • coroutineScope内只要有一个协程抛出了异常,就不再运行后续代码。

    fun main() {
        val exceptionHandler = CoroutineExceptionHandler { _, exception ->
            println("Top-Level CoroutineExceptionHandler got $exception")
        }
        val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")+exceptionHandler
        CoroutineScope(coroutineContext).launch() {
            try {
                coroutineScope{
                    launch {
                        println("This is sub coroutine 1")
                        throw Exception()
                    }
                    launch {
                        println("This is sub coroutine 2")
                    }
                    launch {
                        println("This is sub coroutine 3")
                    }
                }
            } catch (exception: Exception) {
                println("Caught $exception in try-catch block")
            }
        }
        Thread.sleep(100)
    }
    

    运行一下。

    可以看见,子协程1抛出异常后,子协程2和子协程3就没有再运行了。

  • supervisorScope内即使有协程抛出了异常,也会运行后续代码。

    将上述代码中的coroutineScope改成supervisorScope后运行一下。

    可以看见,子协程1抛出异常后,子协程2和子协程3也是照常运行了。

    此外,supervisorScope的异常是抛给了顶级协程的CoroutineExceptionHandler,这点也是和coroutineScope不一样的。

总结

有两种方式处理子协程抛出异常的情况。

  • 设置顶层协程的CoroutineExceptionHandler。
  • 用coroutineScope将子协程包起来

更详细的协程异常处理可以查阅参考资料4。

四.协程使用

协程主要有两种发起方式,launch和async。

launch

launch适用于fire and forget的情况,说人话就是,这种协程发起方式适合执行不需要回调的任务,前面的例子都是用launch,再加上launch比较简单,这里就不再赘述了。

async

async适合的场景刚好与launch相反,适合执行需要回调的任务,上文提到过协程一大亮点就是可以解决地狱回调问题,而这正是通过async方法来实现的。

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("Top-Level CoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")+exceptionHandler
    CoroutineScope(coroutineContext).launch() {
        try {
            val res=async { 
                getDataFromNetWork()//模拟获取网络数据
            }
            parseData(res.await())  //模拟parse获取到的网络数据
        } catch (exception: Exception) {
            println("Caught $exception in try-catch block")
        }
    }
    Thread.sleep(100)
}

async方法会返回一个Deferred,Deferred类有一个await()方法,该方法可以等待结果的返回,从而避免回调地狱。

public interface Deferred<out T> : Job {
    public suspend fun await(): T
}

async用法还是比较简单的,难点在于async的异常处理。

fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("Top-Level CoroutineExceptionHandler got $exception")
    }
    val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")+exceptionHandler
    CoroutineScope(coroutineContext).launch{
        try {
            val res=async {
                println("Exception is about to throw")
                throw Exception()
            }
            println("Before exception is thrown")
            res.await()
        } catch (exception: Exception) {
            println("Caught $exception in try-catch block")
        }
    }
    Thread.sleep(100)
}

不妨猜猜是CoroutineExceptionHandler的handleException()方法会被调用,还是异常会进到catch块。

答案是两个都会。

上述代码中有两处会抛异常。

  • res.await()抛一次,这个异常会被try-catch块处理,只要async{}块里出现了异常,该情况必现。

  • async{}块中的throw Exception()抛一次,这个异常会被CoroutineExceptionHandler处理,但不是async{}块出现异常,异常都会被CoroutineExceptionHandler处理,有以下两种特殊情况:

    • 协程是顶层协程

      fun main() {
          val exceptionHandler = CoroutineExceptionHandler { _, exception ->
              println("Top-Level CoroutineExceptionHandler got $exception")
          }
          val coroutineContext = Dispatchers.Default + CoroutineName("ExceptionHandler")+exceptionHandler
          CoroutineScope(coroutineContext).launch{
              try {
                  val res=GlobalScope.async {//发起顶层协程
                      println("Exception is about to throw")
                      throw Exception()
                  }
                  println("Before exception is thrown")
                  res.await()
              } catch (exception: Exception) {
                  println("Caught $exception in try-catch block")
              }
          }
          Thread.sleep(100)
      }
      

      运行一下。

    • 协程在coroutineScope内发起的。

      这点可查看前面第三部分关于coroutineScope的介绍。

总结一下,async{}块中的异常一般是在await()方法调用的时候抛出来,但当协程为非顶层协程,或者没有用coroutineScope将协程包起来的情况,抛出的异常也就会被CoroutineExceptionHandler处理。

五.写在最后

学习协程这么久,个人感觉协程主要的难点在于

  • 如何理解协程这一概念。
  • 如何进行异常处理。

相比于上面这两点,协程的其他知识点要其实要容易理解得多,本文并没有面面俱到,很多协程的知识点并没有讲到,如suspend函数runBlockingGlobalScopewithContext,但相信各位读者一定能通过其它各种途径学习到。

希望能对你有帮助,有错欢迎留言。

peace。

参考资料

  1. Mastering Kotlin Coroutines In Android - Step By Step Guide
  2. Kotlin Coroutines in Android Summary
  3. CoroutineExceptionHandler not executed when provided as launch context
  4. Why exception handling with Kotlin Coroutines is so hard and how to successfully master it!
  5. kotlin 协程的异常处理
  6. kotlin协程async await的异常踩坑以及异常处理的正确姿势