Kotlin 的 Delimited Continuations 提案,对协程的影响

Kotlin 的 Delimited Continuations 提案,对协程的影响

Kotlin 的 Delimited Continuations 提案,对协程的影响


Kotlin 的 Delimited Continuations 提案,对协程的影响


前几天用 Android Studio 的 "Show Kotlin Bytecode" 看了一段 suspend 函数反编译后的 Java 代码,盯着那个自动生成的 ContinuationImpl 子类看了半天,突然意识到一件事:Kotlin 协程从 1.3 版本稳定到现在,整个大厦的地基其实从来没变过。编译器把 suspend 函数切成一个巨大的状态机,通过 label 字段跳转,把所有局部变量提升为对象字段,然后在 invokeSuspend 里用 switch-case 来回跳。这套 CPS(Continuation-Passing Style)转换足够精巧,但也足够僵硬——你一旦踏进某个 suspend 函数,要么一口气走到底,要么通过异常把整个栈撕碎,没有中间选项。而 JetBrains 现在正在酝酿的 Delimited Continuations 提案,本质上就是想把这块地基撬开一条缝。问题是,这缝撬开之后,获益的究竟是谁,背锅的又是谁?


现在的协程续体,本质是一张"全票"


先别急着谈提案,得把现状说透。现在 Kotlin 编译器处理 suspend 函数时,生成的续体(Continuation)是 undelimited 的。什么意思?比如你写了一个普通的 suspend 函数,里面调了 delay(1000),再调个网络请求,再 withContext(Dispatchers.IO) 写数据库。编译器生成的匿名类会继承 ContinuationImpl,把整个函数的局部变量——哪怕只是中间计算用的一个 Int 临时值——全部装箱成这个续体对象的字段。这个续体一旦创建,它的生命周期就绑定到了整个函数的完结。你要么调用 resumeWith(Result.success(...)) 正常结束,要么抛异常走 resumeWith(Result.failure(...)),不存在"只执行到一半,把中间状态截取出来,稍后换个地方接着跑"这种操作。


这种设计在工程上有个巨大的隐性成本:调试和堆栈追踪仍然是灾难。你去看 kotlinx.coroutines 1.7 或 1.8 版本的源码,DispatchedContinuation 里为了拼接堆栈搞了 CoroutineStackFrame 接口,Android Studio 里看到的协程堆栈依然是碎片化的一地鸡毛。为什么?因为编译器生成的状态机本质上不是真正的调用栈,而是一堆互相 resume 的匿名对象。你捕获的 continuation 是一个"从当前点到函数末尾"的完整票据,票面金额固定,不能找零。


这套模型用了五六年,Room、Retrofit、Ktor、Jetpack Compose 全建在这上面。Google 的 AndroidX 团队把 lifecycle-runtime-ktxrepeatOnLifecycle 做成 suspend 函数,Compose 编译器插件把重组(recomposition)的副作用调度塞进协程上下文,所有代码都默认了一件事:continuation 的边界就是函数边界。现在要引入 Delimited Continuations,等于告诉所有人——那张票可以撕开了,只兑换半程。


Delimited Continuations 要拆的是哪把锁


所谓 Delimited Continuation,就是有界续体。它不像 Scheme 的 call/cc 那样一把抓走整个调用栈直到天地玄黄,而是只捕获从当前执行点到某个显式标记(通常叫 reset 块)之间的计算片段。在这段片段里,你可以把当前的"执行上下文"打包成一个可重用的对象,扔给别的地方,甚至多次调用。这在学术圈和函数式编程语言里不算新鲜,Scala 的 shift/reset、Haskell 的各种 continuation monad transformer,玩的都是这套。


Kotlin 的提案如果落地,语言层面最直观的冲击是:编译器将不再强制把整个 suspend 函数编译成单一的状态机。理论上,你可以在函数内部某个作用域打一个"定界"标记,编译器生成更细粒度的 continuation 片段。这对某些特定场景确实诱人。比如你想实现一个轻量级的 generator/yield 模式,现在 Kotlin 里没有原生实现,得靠硬写状态机或者 sequence builder 模拟;有了 delimited continuation,yield 可以变成真正的控制流劫持。再比如某些服务端场景,Ktor 里处理 HTTP pipeline 时,如果能在中间某个拦截点把 continuation 截断、塞入队列、等资源到位后精准恢复,可能比现在通过 suspend 函数隐式传递上下文要灵活得多。


但这里有个要命的问题:JetBrains 似乎正在把这个特性往语言核心层推,而不仅仅是作为实验性库。如果它进了标准语法或标准库,Kotlin 协程就不再是"带有 suspend 修饰符的简化异步编程"了,它会变成一门拥有完整 first-class continuation 操纵能力的语言。Roman Elizarov 早年设计协程时极力避免的就是让 continuation 直接暴露给普通开发者,只通过 suspendCoroutinesuspendCancellableCoroutine 在库作者层面开放极小口子。这个提案一旦通过,那道口子会被彻底撕开。


Compose 和 Ktor 会被怎么卷进来


首当其冲的是 Jetpack Compose。Compose 的编译器插件对 suspend 函数和状态管理做了大量假设。它依赖 SnapshotState 的读写观测,依赖重组在 CoroutineScope 里的调度顺序。如果底层 continuation 模型变成可截断、可重组的片段,Compose 编译器插件要不要重写?Google 的 Compose 团队和 JetBrains 的 Kotlin 团队虽然合作紧密,但 K2 编译器(Kotlin 2.0)刚把前端架构整利索,Compose 插件迁移到 K2 的过程已经够痛苦了,现在如果续体语义再变,Android 开发者可能面临一个荒诞的局面:升级 Kotlin 版本后,UI 层的重组逻辑出现无法解释的时序 bug,因为编译器生成的 continuation 边界和 Compose runtime 的预期对不上。


Ktor 那边也好不到哪去。服务端开发对请求生命周期的控制极其敏感。现在 Ktor 的 pipeline 用 suspend 函数串起来,每个拦截

Google Play 强制要求 targetSdk 35,适配成本盘点 2026-08-31
SharedUserId 的废弃,多 APK 共享数据怎么办 2026-08-31

评论区