Kotlin 协程是一种轻量级的并发编程工具,它通过挂起函数和协程状态机来实现异步编程。本文将深入探讨 Kotlin 协程的挂起与恢复机制,帮助你更好地理解协程的工作原理。
什么是挂起?
挂起(suspend)是指协程在执行过程中遇到挂起点时,暂停当前协程的执行,并保存其状态,以便稍后恢复执行。挂起点通常是一些耗时操作,如网络请求、I/O 操作或定时器。
挂起函数的实现
挂起函数是协程中的核心概念,它们会在执行过程中挂起协程,并在挂起操作完成后恢复执行。以下是一个常见的挂起函数 delay
的简化实现(伪代码):
kotlin
suspend fun delay(timeMillis: Long) {
// 挂起当前协程,等待指定的时间
return suspendCoroutineUninterceptedOrReturn { continuation ->
// 启动一个定时器,在指定时间后恢复协程
Timer().schedule(timeMillis) {
continuation.resume(Unit)
}
// 返回挂起标记,表示协程已挂起
IntrinsicsKt.getCOROUTINE_SUSPENDED()
}
}
在 delay
函数中,suspendCoroutineUninterceptedOrReturn
是一个低级别的挂起函数,它会挂起当前协程,并返回 IntrinsicsKt.getCOROUTINE_SUSPENDED()
,表示协程已挂起。挂起后,协程的执行会暂停,直到定时器触发并恢复协程。
挂起和恢复的具体体现
让我们通过一个示例代码来展示挂起和恢复的具体过程:
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val repo = UserRepository()
val user1 = async {
repo.getUserById(1)
}
val user2 = async {
repo.getUserById(2)
}
log(user1.await())
log(user2.await())
log("end")
}
class UserRepository {
suspend fun getUserById(id: Int): User {
// 切换到 IO 线程池
return withContext(Dispatchers.IO) {
// 挂起当前协程,等待 1 秒
delay(1000L)
// 恢复执行,返回结果
User(id, "User $id")
}
}
}
data class User(val id: Int, val name: String)
fun log(message: Any?) {
println("[${Thread.currentThread().name}] $message")
}
协程状态机
Kotlin 协程的挂起和恢复是通过协程状态机来实现的。每个挂起函数都会生成一个状态机,该状态机会在挂起点保存协程的当前状态,并在恢复时继续执行。
以下是一个简化的 invokeSuspend
方法的实现,展示了挂起和恢复的过程:
kotlin
public final Object getUserById(final int id, @NotNull Continuation $completion) {
return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
// 初始状态,检查是否有异常
ResultKt.throwOnFailure($result);
// 设置下次恢复时的状态
this.label = 1;
// 调用 delay 方法,挂起协程
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 new User(id, "User " + id);
}
@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);
}
}), $completion);
}
详细执行过程
-
启动
runBlocking
:runBlocking
启动一个新的协程,并阻塞当前线程,直到协程完成。
-
启动
async
协程:async
启动两个新的协程,分别调用repo.getUserById(1)
和repo.getUserById(2)
。
-
调用
getUserById
:getUserById
方法被调用,进入withContext
块,切换到Dispatchers.IO
线程池。invokeSuspend
方法被调用,label
的初始值为0
。
-
第一次执行
invokeSuspend
:label
为0
,进入case 0
。- 调用
ResultKt.throwOnFailure($result)
检查是否有异常。 - 设置
label
为1
,表示下次恢复时从case 1
开始执行。 - 调用
DelayKt.delay(1000L, this)
模拟网络请求的延迟。 delay
方法返回IntrinsicsKt.getCOROUTINE_SUSPENDED()
,表示协程已挂起。invokeSuspend
方法返回IntrinsicsKt.getCOROUTINE_SUSPENDED()
,协程暂停执行。
-
挂起状态:
- 协程挂起,等待
delay
方法完成。
- 协程挂起,等待
-
恢复执行:
delay
方法完成后,协程恢复执行。- 恢复时,
invokeSuspend
方法再次被调用,这次label
的值为1
。
-
第二次执行
invokeSuspend
:label
为1
,进入case 1
。- 调用
ResultKt.throwOnFailure($result)
检查是否有异常。 - 协程继续执行,创建并返回一个新的
User
对象:new User(id, "User " + id)
。
-
等待
async
结果:user1.await()
和user2.await()
分别等待user1
和user2
协程的结果。await
方法会挂起当前协程,直到user1
和user2
协程完成并返回结果。
-
打印结果:
log(user1.await())
和log(user2.await())
分别打印user1
和user2
的结果。log("end")
打印 "end"。
总结
挂起在函数中的具体体现是通过返回 IntrinsicsKt.getCOROUTINE_SUSPENDED()
来实现的。当协程遇到挂起点时,会返回这个特殊的标记值,表示协程已挂起。挂起后,协程的执行会暂停,当前的执行状态会被保存下来,等待挂起操作完成后再恢复执行。恢复时,协程会从挂起点继续执行,恢复之前保存的状态。