Skip to content

Latest commit

 

History

History
165 lines (125 loc) · 9.38 KB

2.Kotlin协程之Continuation和suspendCoroutine.md

File metadata and controls

165 lines (125 loc) · 9.38 KB

2.Kotlin协程之Continuation和suspendCoroutine

目录


Continuation的作用

前面我们讲挂起函数的时候,讲过:suspend函数经过编译之后会多出来一个Continuation<T>类型的参数。本篇文章将详细探讨一下,Continuation是干啥的。

主要作用有2个:

  1. 实现挂起函数:在挂起函数的内部使用Continuation这个callback传递数据给外部
  2. 调用挂起函数时的传参:在调用挂起函数的时候,需要传递一个Continuation,用来接收挂起函数执行结果

实现挂起函数时,一般会需要用到suspendCoroutine或者suspendCancellableCoroutine,它们用法差不多,只是suspendCancellableCoroutine提供了一个CancellableContinuation,可以手动取消。谷歌建议我们尽可能使用suspendCancellableCoroutine,因为协程的取消是可控的。

因为用法差别不大,我这里使用suspendCoroutine来举例子,实现挂起函数:

fun main(): Unit = runBlocking {
    val name = getUserName()
    println(name)
}

suspend fun getUserName() = suspendCoroutine { continuation ->
    thread {
        Thread.sleep(2000L)
        continuation.resume("云天明")
    }
}

我在suspendCoroutine内部开了个线程,等待2秒后将结果通过continuation回调出去。而在runBlocking中,它等着getUserName执行完,拿到结果之后才开始执行后面的println语句。在挂起函数getUserName的内部,continuation的作用就是将挂起函数的执行结果传递出去。

思考一个问题,它是怎么拿到结果的?我明明是将结果通过continuation回调出去的,为啥外面却可以直接拿到结果了,而没有实现类似callback的东西。看过Kotlin挂起函数原理这篇文章的同学可能已经知道我要说什么了,实际上,在调用getUserName函数的地方,是传入了一个continuation的。在getUserName内部通过resume返回结果时,会回调传入的continuation的invokeSuspend方法,接着继续执行runBlocking后面的逻辑,也就是继续状态机的执行。

suspendCoroutine的原理

这里实现挂起函数的重点是suspendCoroutine,它和suspendCancellableCoroutine都是Kotlin协程的基础元素,在Kotlin库中。它们2个原理差不多,我们分析一下suspendCoroutine就行,看一下它的源码长什么样:

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

在内部,主要是调用了一下suspendCoroutineUninterceptedOrReturn这个高阶函数来实现主要逻辑,其他部分都是很明显的:

  1. 将Continuation包装了一下,包成SafeContinuation
  2. 调用传入的Lambda,并且将safe作为参数传进去
  3. 获取Continuation的值,或者抛出异常

我们重点关注一下suspendCoroutineUninterceptedOrReturn:

public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (Continuation<T>) -> Any?): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    throw NotImplementedError("Implementation of suspendCoroutineUninterceptedOrReturn is intrinsic")
}

???卧槽?这个方法内居然没有源码,而是直接抛出异常,但是我们明明执行到它了啊,并没有抛异常。为啥?实际上,我们仔细看一下抛出的异常,它想表达的是这个方法是由Kotlin编译器来实现的。所以,我们只需要写个demo,来看下编译之后长什么样子,就行了。注意,这里的block,也就是传入的函数类型是(Continuation<T>) -> Any?,这刚好就是上节课我们发现的suspend函数经过CPS转换之后的样子,Any?表示可能会直接返回数据,或者返回COROUTINE_SUSPENDED

既然suspendCoroutineUninterceptedOrReturn需要的是(Continuation<T>) -> Any?,那我们在代码中使用该一下suspendCoroutineUninterceptedOrReturn,并且给它一个(Continuation<T>) -> Any?,然后再反编译,不就拿到了它编译之后长什么样子了么,就像下面这样:

fun main(): Unit = runBlocking {
    val name = getUserName()
    println(name)
}

//直接写suspendCoroutineUninterceptedOrReturn居然不会提示导包,需要手动导包:import kotlin.coroutines.intrinsics.*
//                                                              这里<String>可以不用写,可以推导出来,咱学东西时还是把这个写上,知道有这个东西
suspend fun getUserName() = suspendCoroutineUninterceptedOrReturn<String> { continuation ->
    return@suspendCoroutineUninterceptedOrReturn "云天明"
}

注意,此处我并没有使用Continuation.resume来将结果回调出去,而是直接通过return将结果返回出去,所以它现在不是一个真正的挂起函数。

反编译一下,看看长什么样子:

public static final Object getUserName(@NotNull Continuation $completion) {
  int var2 = false;
  if ("云天明" == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
     DebugProbesKt.probeCoroutineSuspended($completion);
  }

  return "云天明";
}

反编译之后,可以看到,suspendCoroutineUninterceptedOrReturn编译出来的东西非常少,仅仅是将云天明返回出去。在return出去之前,它拿去和COROUTINE_SUSPENDED进行比较,可以看出它是想判断一下该函数是不是真正的挂起函数,如果是挂起函数的话,是会返回COROUTINE_SUSPENDED的。下面我们就来写一个真正的挂起函数:

fun main(): Unit = runBlocking {
    val name = getUserName()
    println(name)
}

suspend fun getUserName() = suspendCoroutineUninterceptedOrReturn<String> { continuation ->
    thread {
        Thread.sleep(2000L)
        continuation.resume("云天明")
    }
    return@suspendCoroutineUninterceptedOrReturn COROUTINE_SUSPENDED
}

真正的挂起函数应该立刻返回COROUTINE_SUSPENDED,而后再慢慢的去计算,计算完了再通过continuation返回真实的结果。来看下反编译之后长什么样子:

public static final Object getUserName(@NotNull Continuation $completion) {
  int var2 = false;
  //创建线程
  ThreadsKt.thread$default(false, false, (ClassLoader)null, (String)null, 0, (Function0)(new SuspendTestKt$getUserName$2$1($completion)), 31, (Object)null);
  Object var10000 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
  if (var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
     DebugProbesKt.probeCoroutineSuspended($completion);
  }
  
  //直接返回COROUTINE_SUSPENDED,表示该函数已挂起
  return var10000;
}

//这个类是一个匿名内部类,创建来是为了传给thread,创建线程之后就会执行这个Lambda
final class SuspendTestKt$getUserName$2$1 extends Lambda implements Function0<Unit> {
    final  Continuation<String> $continuation;

    SuspendTestKt$getUserName$2$1(Continuation<? super String> continuation) {
        super(0);
        this.$continuation = continuation;
    }

    public final void invoke() {
        //这块代码是在新开的一个线程中执行的
        Thread.sleep(2000);
        Continuation<String> continuation = this.$continuation;
        Result.Companion companion = Result.Companion;
        //sleep 2秒之后将结果返回出去,通过continuation
        continuation.resumeWith(Result.m23constructorimpl("云天明"));
    }
}

可以看到,这就是我们写的代码,就多了个continuation参数,其他什么也没变。我们首先是创建了一个线程,然后将continuation参数传入线程的Lambda中,随后马上就返回了COROUTINE_SUSPENDED,表示该函数已挂起。线程执行完成之后,通过continuation.resume将结果回调出去。

好了,现在我们知道suspendCoroutineUninterceptedOrReturn有什么用处了,它其实就是把Continuation这个参数暴露出来,让开发者可以使用(getUserName内部)。如果我们要实现真正的挂起函数的话,则需要返回COROUTINE_SUSPENDED才行。

是不是有一种感觉,这suspendCoroutineUninterceptedOrReturn听着咋感觉有点像suspendCoroutine和suspendCancellableCoroutine呢。 没错,suspendCoroutine和suspendCancellableCoroutine其实就是包装了suspendCoroutineUninterceptedOrReturn,功能都是一样的。

小结

  • Continuation可以它简单的看作Callback。实现挂起函数的时候,可以通过continuation向外部回调数据。调用挂起函数时,会传一个Continuation过去,用来接收数据,且执行后面的逻辑。
  • suspendCoroutine和suspendCancellableCoroutine其实就是包装了suspendCoroutineUninterceptedOrReturn。suspendCoroutineUninterceptedOrReturn会暴露一个Continuation出来,方便实现挂起(return COROUTINE_SUSPENDED)和 通过Continuation.resume向外传递数据。suspendCoroutineUninterceptedOrReturn包装成suspendCoroutine和suspendCancellableCoroutine是为了方便开发者使用,降低上手门槛。