Kotlin 协程的取消(二)理解协程的取消原理(草稿)

130 阅读2分钟

关于协程的取消机制,很明显和 suspend 关键字有关。为了测试 suspend 关键字的作用,实现下面的代码:

class Solution {
    suspend fun func(): String {
        return "测试 suspend 关键字"
    }
}

作为对照组,另一个是不加 suspend 关键字的 func 方法:

class Solution {
    fun func(): String {
        return "测试 suspend 关键字"
    }
}

两者反编译成 Java :

// 普通的方法
public final class Solution {
    public static final int $stable = LiveLiterals$SolutionKt.INSTANCE.Int$class-Solution();

    @NotNull
    public final String func() {
        return LiveLiterals$SolutionKt.INSTANCE.String$fun-func$class-Solution();
    }
}

// 带有 suspend 关键字的方法
public final class Solution {
    public static final int $stable = LiveLiterals$SolutionKt.INSTANCE.Int$class-Solution();

    @Nullable
    public final Object func(@NotNull Continuation<? super String> $completion) {
        return LiveLiterals$SolutionKt.INSTANCE.String$fun-func$class-Solution();
    }
}

suspend 关键字修饰的方法反编译后默认生成了带有 Continuation 参数的方法。说明 suspend 关键字的玄机在 Continuation 类中。

Continuation 是 Kotlin 协程的核心思想 Continuation-Passing Style 的实现方案。通过在普通函数的参数中增加一个Continuation 参数,这个 continuation 的性质类似于一个 lambda 对象,将方法的返回值类型传递到这个 lambda 代码块中。

什么意思呢?就是本来这个方法的返回类型直接 return 出来的:

val a: String = func()
print(a)

而经过 suspend 修饰,代码变成了这个样子:

func { a ->
    print(a)
}

Kotlin 协程就是通过这样的包装,将比如 launch 方法,实际上是 launch 最后一个参数接收的是 lambda 参数。也就是把外部逻辑传递给函数内部执行。

关于 Continuation-Passing Style 参考: juejin.cn/post/704223…

回过头来再来理解 suspend 关键字,我们知道带有 suspend 关键字的方法会对协程的取消进行检查,从而取消协程的执行。从这个能力上来看,我理解他应该会自动生成类似下面的逻辑代码:

生成的函数 {
    if(!当前协程.isActive) {
        throw CancellationException()
    }
    // ... 这里是函数真实逻辑
}

suspend 修饰的函数,会自动生成一个挂起点,来检查协程是否应该被挂起。

显然 Continuation 中声明的函数也证实了挂起的功能:

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

    /**
     * 恢复相应协程的执行,将成功或失败的结果作为最后一个挂起点的返回值传递。
     */
    public fun resumeWith(result: Result<T>)
}

协程本质上是产生了一个 switch 语句,每个挂起点之间的逻辑都是一个 case 分支的逻辑。参考 协程是如何实现的 中的例子:

        Function1 lambda = (Function1)(new Function1((Continuation)null) {
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
                byte text;
                @BlockTag1: {
                    Object result;
                    @BlockTag2: {
                        result = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                            case 0:
                                ResultKt.throwOnFailure($result);
                                this.label = 1;
                                if (SuspendTestKt.dummy(this) == result) {
                                    return result;
                                }
                                break;
                            case 1:
                                ResultKt.throwOnFailure($result);
                                break;
                            case 2:
                                ResultKt.throwOnFailure($result);
                                break @BlockTag2;
                            case 3:
                                ResultKt.throwOnFailure($result);
                                break @BlockTag1;
                            default:
                                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }

                        text = 1;
                        System.out.println(text);
                        this.label = 2;
                        if (SuspendTestKt.dummy(this) == result) {
                            return result;
                        }
                    }

                    text = 2;
                    System.out.println(text);
                    this.label = 3;
                    if (SuspendTestKt.dummy(this) == result) {
                        return result;
                    }
                }
                text = 3;
                System.out.println(text);
                return Unit.INSTANCE;
            }

            @NotNull
            public final Continuation create(@NotNull Continuation completion) {
                Intrinsics.checkNotNullParameter(completion, "completion");
                Function1 funcation = new <anonymous constructor>(completion);
                return funcation;
            }

            public final Object invoke(Object object) {
                return ((<undefinedtype>)this.create((Continuation)object)).invokeSuspend(Unit.INSTANCE);
            }
        });

可以看出,在每个分支都会执行一次 ResultKt.throwOnFailure($result); ,似乎这个函数应该就是上面伪代码的实现。

@PublishedApi
@SinceKotlin("1.3")
internal fun Result<*>.throwOnFailure() {
    if (value is Result.Failure) throw value.exception
}

这里的 Result 类是一个包装类,它将成功的结果封装为类型 T 的值,或将失败的结果封装为带有任意Throwable异常的值。

        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("success")
        public inline fun <T> success(value: T): Result<T> =
            Result(value)

        /**
         * Returns an instance that encapsulates the given [Throwable] [exception] as failure.
         */
        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("failure")
        public inline fun <T> failure(exception: Throwable): Result<T> =
            Result(createFailure(exception))

成功和失败的方法类型是不一样的,证实了这一点,success 方法接收类型为 T 的参数;failure 接收 Throwable 类型的参数。

到这里 suspend 方法挂起的原理就明了了:**在协程的状态机中,挂起点会分割出不同的状态,对每一个状态,会先进行挂起结果的检查。**这会导致以下结果:

  • 协程的取消机制是通过挂起函数的挂起点检查来进行取消检查的。证实了为什么如果没有 suspend 函数(本质是挂起点),协程的取消就不会生效。
  • 协程的取消机制是需要函数合作的,就是通过 suspend 函数来增加取消检查的时机。
  • 父协程会执行完所有的子协程(挂起函数),因为代码的本质是一个循环执行 switch 语句,当一个子协程(或挂起函数)执行结束,会继续执行到下一个分支。但是最后一个挂起点后续的代码并不会被执行,因为最后一个挂起点检查到失败,不会继续跳到最后的 label 分支。