深入理解 Kotlin 协程的挂起与恢复机制

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);
}

详细执行过程

  1. 启动 runBlocking:

    • runBlocking 启动一个新的协程,并阻塞当前线程,直到协程完成。
  2. 启动 async 协程:

    • async 启动两个新的协程,分别调用 repo.getUserById(1) 和 repo.getUserById(2)。
  3. 调用 getUserById:

    • getUserById 方法被调用,进入 withContext 块,切换到 Dispatchers.IO 线程池。
    • invokeSuspend 方法被调用,label 的初始值为 0。
  4. 第一次执行 invokeSuspend:

    • label 为 0,进入 case 0。
    • 调用 ResultKt.throwOnFailure($result) 检查是否有异常。
    • 设置 label 为 1,表示下次恢复时从 case 1 开始执行。
    • 调用 DelayKt.delay(1000L, this) 模拟网络请求的延迟。
    • delay 方法返回 IntrinsicsKt.getCOROUTINE_SUSPENDED(),表示协程已挂起。
    • invokeSuspend 方法返回 IntrinsicsKt.getCOROUTINE_SUSPENDED(),协程暂停执行。
  5. 挂起状态:

    • 协程挂起,等待 delay 方法完成。
  6. 恢复执行:

    • delay 方法完成后,协程恢复执行。
    • 恢复时,invokeSuspend 方法再次被调用,这次 label 的值为 1。
  7. 第二次执行 invokeSuspend:

    • label 为 1,进入 case 1。
    • 调用 ResultKt.throwOnFailure($result) 检查是否有异常。
    • 协程继续执行,创建并返回一个新的 User 对象:new User(id, "User " + id)。
  8. 等待 async 结果:

    • user1.await() 和 user2.await() 分别等待 user1 和 user2 协程的结果。
    • await 方法会挂起当前协程,直到 user1 和 user2 协程完成并返回结果。
  9. 打印结果:

    • log(user1.await()) 和 log(user2.await()) 分别打印 user1 和 user2 的结果。
    • log("end") 打印 "end"。

总结

挂起在函数中的具体体现是通过返回 IntrinsicsKt.getCOROUTINE_SUSPENDED() 来实现的。当协程遇到挂起点时,会返回这个特殊的标记值,表示协程已挂起。挂起后,协程的执行会暂停,当前的执行状态会被保存下来,等待挂起操作完成后再恢复执行。恢复时,协程会从挂起点继续执行,恢复之前保存的状态。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui