Kotlin 协程挂起函数原理
挂起函数
Kotlin 编译器将使用协程的代码转换成状态机的形式。这一过程主要涉及到挂起函数的调用。
通过一个示例来探讨挂起函数:
class ExampleUnitTest_CoroutineCore { @Test fun test() { runBlocking { val scope = CoroutineScope(Job()) val job = scope.launch { val msg = getNewMessage() println(msg) } job.join() } } suspend fun getNewMessage(): String { return withContext(Dispatchers.IO) { delay(1000) "12345" } } /** * suspend 是灰色,如果内部没有其他挂起函数,那么编译器就会提示我们移除 suspend 标记 */ suspend fun testSuspend() {}}反编译字节码如下:
public final class ExampleUnitTest_CoroutineCore { @Test public final void test() { BuildersKt.runBlocking$default((CoroutineContext)null, (Function2)(new Function2((Continuation)null) { int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch (this.label) { case 0: ResultKt.throwOnFailure($result); CoroutineScope scope = CoroutineScopeKt.CoroutineScope((CoroutineContext)JobKt.Job$default((Job)null, 1, (Object)null)); Job job = BuildersKt.launch$default(scope, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) { // $FF: synthetic field private Object L$0; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); Object var10000; CoroutineScope $this$launch; switch (this.label) { case 0: ResultKt.throwOnFailure($result); $this$launch = (CoroutineScope)this.L$0; ExampleUnitTest_CoroutineCore var5 = ExampleUnitTest_CoroutineCore.this; this.L$0 = $this$launch; this.label = 1; var10000 = var5.getNewMessage(this); if (var10000 == var4) { return var4; } break; case 1: $this$launch = (CoroutineScope)this.L$0; ResultKt.throwOnFailure($result); var10000 = $result; break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } String msg = (String)var10000; LogExtKt.println($this$launch, msg); return Unit.INSTANCE; } @NotNull public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function2 var3 = new <anonymous constructor>(completion); var3.L$0 = value; return var3; } public final Object invoke(Object var1, Object var2) { return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); } }), 3, (Object)null); this.label = 1; if (job.join(this) == var4) { return var4; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return Unit.INSTANCE; } @NotNull public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function2 var3 = new <anonymous constructor>(completion); return var3; } public final Object invoke(Object var1, Object var2) { return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); } }), 1, (Object)null); } @Nullable public final Object getNewMessage(@NotNull Continuation $completion) { return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) { int label; // 默认 label=0 // 调用挂起 @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch (this.label) { case 0: ResultKt.throwOnFailure($result); // throwOnFailure:如果结果失败则抛出异常,可以理解为对挂起函数做检测(取消也会抛出 CancellationException 异常) this.label = 1; // var2 是 COROUTINE_SUSPENDED,返回 var2 说明函数是挂起状态(相当于告诉父 callback,当前我内部已经在忙了,你可以先执行自己的事了,等我执行完再通知你) // this 是 CancellableContinuation,在延迟任务执行完后通过 CancellableContinuation 回调回来(再次触发 invokeSuspend),第二次 label 已被修改为了 1; if (DelayKt.delay(1000L, this) == var2) { return var2; } break; case 1: ResultKt.throwOnFailure($result); // 与上面一样,做异常检测,检测到异常抛出异常退出函数执行 break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "12345"; // label=1,没发生异常,最终走到这里 } @NotNull public final Continuation create(@Nullable Object value, @NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function2 var3 = new <anonymous constructor>(completion); return var3; } // Function2 接口的 invoke 函数 // 通过 create 创建一个 Continuation 对象,用于执行协程体逻辑,然后去调用 invokeSuspend() 方法从而获得本次的执行结果 // 已经有了 (Continuation)var2 为什么还要再创建一个? // 因为 var2 BuildersKt.launch 的 Function2 匿名内部类创建并传递过来的,便于 getNewMessage 执行结果向上返回。类似于 func A ->func B(callbackA)->func C(callbackB) public final Object invoke(Object var1, Object var2) { return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE); } }), $completion); } /** * Object 类型返回值用于确定该挂起函数是否真的被挂起 * 返回 Unit.INSTANCE 不需要挂起, */ @Nullable public final Object testSuspend(@NotNull Continuation $completion) { return Unit.INSTANCE; }}从反编译字节码可以清晰看出挂起函数挂起和恢复的流程。挂起函数实现的本质还是回调,只是这层回调是编译器帮我们做了。
挂起与恢复的实现
挂起和恢复的实现是通过 Continuation(续体)来完成的。每个挂起函数都接受一个 Continuation 参数,这个参数包含了协程在挂起点之后继续执行的所有信息。
- Continuation:续体是协程挂起和恢复的关键。它持有协程的上下文信息,包括协程在挂起时的状态和恢复执行时应当执行的代码位置。
- 回调的转换:挂起函数通常会将异步操作转换为同步操作的形式。这是通过将 Continuation 传递给异步操作,然后在异步操作完成时调用 Continuation 的 resumeWith 方法来实现的。
- 状态机转换:编译器将含有挂起点的协程代码转换成一个状态机(其实就 0、1 两个状态)。每个挂起点都对应状态机中的一个状态。编译器为协程生成的代码负责在挂起点之间转移,并在适当的时候恢复执行。
public interface Continuation<in T> { /** * 协程的上下文 */ public val context: CoroutineContext /** * 恢复相应协程的执行,传递成功或失败的结果作为最后一个挂起点的返回值。 */ public fun resumeWith(result: Result<T>)}