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 处时:
- 保存局部、设置下一 label;
- 返回哨兵 COROUTINE_SUSPENDED;
- 未来某时你调用 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) 常见坑 & 排错清单
- 忘了只恢复一次:并发回调/取消同时到达,直接 resume 会偶发崩;用 tryResume* + completeResume 或原子标志。
- 未注册取消清理:suspendCoroutine/错误用法导致底层请求泄漏;优先选 suspendCancellableCoroutine 并 invokeOnCancellation。
- 先发再挂钩:在注册 invokeOnCancellation 之前就发起操作,可能错过早期取消 → 先挂钩,再发起。
- 多次添加监听 但只移除一次:确保在成功/失败与取消路径都对称移除监听。
- 把多次事件硬塞这俩 API:会丢后续事件或阻塞;多事件请用 callbackFlow。
- 把异常吞掉:一定要在失败回调里 resumeWithException(e),否则协程会"卡死"。
- 清理里做重活:invokeOnCancellation 回调运行在线程不确定处,避免重活/阻塞 UI;必要时切到合适的 dispatcher。
9) 何时仍可用suspendCoroutine
-
底层操作不可取消(例如一次性 JNI 调用、系统同步调用包了个异步壳,但实际瞬时返);
-
或取消对底层无意义(就算取消也无法停止/无需清理);
-
或你在外层已经严格超时/隔离了调用(比如外部加了进程级超时/沙盒)。
但一旦有资源占用/监听/I/O ------请默认用 suspendCancellableCoroutine。
10) 速记
-
桥接回调 → suspend:用 suspendCancellableCoroutine;取消就 invokeOnCancellation{...}。
-
只恢复一次:tryResume / tryResumeWithException + completeResume。
-
多事件:callbackFlow,不是这俩。
-
线程无忧:随处 resume,恢复线程交给 dispatcher。
-
先挂钩再发起 ,成功/失败/取消路径对称清理。