日常笔记,内容粗糙,敬请谅解
“
关于 Kotlin 协程的原理理解和 Continuation-Passing Style ,请参考:协程是如何实现的
关于协程的取消机制,很明显和 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 参考:https://juejin.cn/post/7042233212302524423
回过头来再来理解 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 分支。
原文始发于微信公众号(八千里路山与海):Kotlin 协程的取消(二)理解协程的取消原理
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/85032.html