Skip to content

安全恢复:SafeContinuation

源:Kotlin 标准库内部机制

协程框架通过 SafeContinuation 保证了一个核心契约:每个 Continuation 的 resume 必须且只能调用一次

为什么需要保护?

在极其复杂的异步场景下(如从第三方 Callback 转换,或涉及多线程竞争),可能会错误地调用两次 resume

  • 如果不加保护:会导致程序崩溃 (IllegalStateException: Already resumed) 或逻辑错乱。
  • 使用 SafeContinuation:它会原子性地检查状态,确保只有第一次 resume 生效,或者抛出更有意义的异常。

内部状态机

SafeContinuation 内部维护一个 result 字段,该字段充当状态机:

状态值含义
UNDECIDED初始状态,还没有任何结果。
SUSPENDED协程已真正挂起(返回了 COROUTINE_SUSPENDED)。
RESUMED协程已被恢复。

运作流程

场景 1:真正的异步挂起

  1. 调用 suspendCoroutine
  2. 内部将状态置为 SUSPENDED
  3. 函数返回 COROUTINE_SUSPENDED,线程释放。
  4. 异步回调触发,调用 resume()
  5. CAS 操作发现状态是 SUSPENDED,将其更新为结果,并唤醒协程

场景 2:同步直接返回 (未挂起)

  1. 调用 suspendCoroutine
  2. 但在 block 内部立即调用了 resume()
  3. CAS 操作发现状态是 UNDECIDED,直接将结果存入字段,不唤醒(因为还没挂起呢)。
  4. suspendCoroutine 检查到已有结果,直接返回该结果,不发生挂起。
    • 这就是为什么 suspendCoroutine 也可以处理同步逻辑。

开发者视角

在日常开发中,建议使用 suspendCancellableCoroutine,它内部默认使用了 SafeContinuation 机制,并且额外支持了被取消时的响应invokeOnCancellation)。

kotlin
suspend fun <T> Call<T>.await(): T = suspendCancellableCoroutine { cont ->
    enqueue(object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            // SafeContinuation 保证这里的 resume 是安全的
            cont.resume(response.body()!!) 
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            cont.resumeWithException(t)
        }
    })
    
    // 如果协程被取消,取消 HTTP 请求
    cont.invokeOnCancellation { cancel() }
}