协程真正的“挂起点”:suspendCoroutine 与 suspendCancellableCoroutine

1) 它们在协程体系里的位置

  • 作用 :把回调式/异步 API"桥接"成 suspend,从而能写出同步风格。
  • 本质 :在这里创建并拿到当前挂起点的 Continuation ,由你在未来某个时刻调用 resume/resumeWithException 让协程从该点继续
  • 单次信号 :这俩函数都适合"一次完成/失败"的异步操作;多次事件应使用 callbackFlow{}。

2) 签名与关键差异(先记住这三条)

函数 传给你的 continuation 取消响应 场景
suspendCoroutine { cont: Continuation -> } 不暴露可取消特性(看不到 Job) 不会帮你取消底层操作 ;协程被取消时通常仍会收到 CancellationException 让它结束,但你拿不到钩子做清理 底层操作不可取消 / 很快返回、或无需清理
suspendCancellableCoroutine { cont: CancellableContinuation -> } CancellableContinuation 协程取消会立刻取消这个 continuation ,你还可 invokeOnCancellation{...} 取消/清理底层操作 绝大多数可耗时、可取消或需清理资源的桥接

口诀:能用 suspendCancellableCoroutine 就别用 suspendCoroutine 。后者只有在你确实不需要感知/转发取消时才用。


3) 编译/运行期要点(为什么它能"挂起")

  • 编译器把 suspend 改写成状态机(有 label 与局部变量槽位),到达这两个 API 处时:

    1. 保存局部、设置下一 label;
    2. 返回哨兵 COROUTINE_SUSPENDED;
    3. 未来某时你调用 cont.resume(...) / resumeWithException(e),调度器据此恢复到该 label 之后继续跑。
  • suspendCancellableCoroutine 里的 cont 已经被拦截成可取消的 DispatchedContinuation,能与 Job 协作(见下文)。


4) 取消传播与清理(核心差异的落点)

suspendCoroutine

  • 协程自身 被取消时,最终也会以 CancellationException 结束这段挂起,但你拿不到取消回调 去做 底层 操作的 cancel()/dispose();

  • 结果:底层请求可能继续"暗自运行" (泄漏网络/传感器/监听),直到自然完成或自行报错。

suspendCancellableCoroutine

  • 进入挂起后,若父 Job 取消或 withTimeout 触发:

    • 这个 continuation 立刻进入取消态,你可通过
arduino 复制代码
cont.invokeOnCancellation { /* 取消底层请求/移除监听/释放句柄 */ }
    • 做清理/撤销;
    • 你也能用 cont.isActive / isCancelled 判断当前能否继续 resume。
  • 早已取消 进入的场景:即使进入 block 前父协程已取消,block 仍会被调用(让你注册清理 ),随后函数抛出 CancellationException 结束------这正是你仍需写 invokeOnCancellation 的原因

5)只恢复一次:并发/竞态下的正确写法

  • 任何 continuation 只能恢复一次;重复 resume 会抛 IllegalStateException。
  • 可能的竞态:成功回调失败/取消回调几乎同时到达。
  • 解决:可取消版本提供了原子 API:
kotlin 复制代码
val token = cont.tryResume(value) ?: return  // 已被取消/已恢复就直接返回
cont.completeResume(token)
  • 对异常同理:tryResumeWithException(e) / completeResume(token)。
  • 若用 suspendCoroutine,你只能用 AtomicBoolean/compareAndSet 自己兜底。

6) 三个"模板"直接拿走就用

6.1 基础不可取消桥接(短平快)

kotlin 复制代码
suspend fun awaitOnce(): T = suspendCoroutine { cont ->
    api.callOnce(
        onSuccess = { v -> cont.resume(v) },
        onError   = { e -> cont.resumeWithException(e) }
    )
}

适用于即时完成/失败,或即便不取消也无害的 API。


6.2 标准可取消桥接(最常用)

kotlin 复制代码
suspend fun Call.await(): Response = suspendCancellableCoroutine { cont ->
    // 1) 取消时的清理/撤销(必须先注册)
    cont.invokeOnCancellation { cancel() }   // OkHttp 示例:取消网络请求

    // 2) 发起操作
    enqueue(object : Callback {
        override fun onResponse(call: Call, resp: Response) {
            // 3) 原子恢复,避免与取消/失败竞态
            val token = cont.tryResume(resp) ?: return
            cont.completeResume(token)
        }
        override fun onFailure(call: Call, e: IOException) {
            // 如果是因为取消导致的错误,通常可以直接 return
            if (cont.isCancelled) return
            val token = cont.tryResumeWithException(e) ?: return
            cont.completeResume(token)
        }
    })
}

要点:

  • 先注册 invokeOnCancellation,再发起操作,避免"刚发就被取消却没清理"的窗口。
  • 使用 tryResume* + completeResume 防双写。

6.3 可取消 + 移除监听(典型监听型 API)

kotlin 复制代码
suspend fun <T> EventSource<T>.awaitNext(): T =
    suspendCancellableCoroutine { cont ->
        val listener = object : Listener<T> {
            override fun onEvent(v: T) {
                val token = cont.tryResume(v) ?: return
                cont.completeResume(token)
                removeListener(this) // 只要一次
            }
            override fun onError(e: Throwable) {
                val token = cont.tryResumeWithException(e) ?: return
                cont.completeResume(token)
                removeListener(this)
            }
        }
        addListener(listener)
        cont.invokeOnCancellation { removeListener(listener) } // 取消时解绑
    }

7) 与调度器/线程的关系

  • 你在任意线程调用 cont.resume(...) 都可以;真正继续执行代码的线程由**协程上下文的 ContinuationInterceptor(dispatcher)**决定(可能 inline,也可能派发到 Main/IO)。
  • 若你强依赖在当前线程立即执行首段,可配合 CoroutineStart.UNDISPATCHED 或 Dispatchers.Main.immediate 等控制首段恢复策略(见你之前学过的调度章节)。

8) 常见坑 & 排错清单

  1. 忘了只恢复一次:并发回调/取消同时到达,直接 resume 会偶发崩;用 tryResume* + completeResume 或原子标志。
  2. 未注册取消清理:suspendCoroutine/错误用法导致底层请求泄漏;优先选 suspendCancellableCoroutine 并 invokeOnCancellation。
  3. 先发再挂钩:在注册 invokeOnCancellation 之前就发起操作,可能错过早期取消 → 先挂钩,再发起。
  4. 多次添加监听 但只移除一次:确保在成功/失败与取消路径都对称移除监听
  5. 把多次事件硬塞这俩 API:会丢后续事件或阻塞;多事件请用 callbackFlow。
  6. 把异常吞掉:一定要在失败回调里 resumeWithException(e),否则协程会"卡死"。
  7. 清理里做重活:invokeOnCancellation 回调运行在线程不确定处,避免重活/阻塞 UI;必要时切到合适的 dispatcher。

9) 何时仍可用suspendCoroutine

  • 底层操作不可取消(例如一次性 JNI 调用、系统同步调用包了个异步壳,但实际瞬时返);

  • 或取消对底层无意义(就算取消也无法停止/无需清理);

  • 或你在外层已经严格超时/隔离了调用(比如外部加了进程级超时/沙盒)。

但一旦有资源占用/监听/I/O ------请默认用 suspendCancellableCoroutine


10) 速记

  • 桥接回调 → suspend:用 suspendCancellableCoroutine;取消就 invokeOnCancellation{...}。

  • 只恢复一次:tryResume / tryResumeWithException + completeResume。

  • 多事件:callbackFlow,不是这俩。

  • 线程无忧:随处 resume,恢复线程交给 dispatcher。

  • 先挂钩再发起 ,成功/失败/取消路径对称清理

相关推荐
绝无仅有3 小时前
MySQL 面试题及详细解答(二)
后端·面试·github
Terio_my4 小时前
Java 后端面试技术文档(参考)
java·开发语言·面试
用户47949283569155 小时前
一道原型链面试题引发的血案:为什么90%的人都答错了
前端·javascript·面试
Lotzinfly6 小时前
10个React性能优化奇淫技巧你需要掌握😏😏😏
前端·react.js·面试
uhakadotcom6 小时前
分享近期学到的postgresql的几个实用的新特性!
后端·面试·github
UrbanJazzerati6 小时前
使用Mockoon快速搭建Mock API:从入门到实战
前端·面试
知其然亦知其所以然6 小时前
MySQL 社招必考题:如何优化WHERE子句?
后端·mysql·面试
召摇6 小时前
Java Web开发从零开始:初学者完整学习指南
java·后端·面试
南北是北北6 小时前
协程async vs launch 的异常与结果学
面试