协程中的Deferred

44 阅读3分钟

1) 它是什么:带“结果”的 Job

  • Deferred : Job —— Job + 一个将来可用的结果 T

  • 典型来源:async { /* 计算并 return T */ }。

  • 读取结果的“正门”是 await() (挂起直到得到 T 或抛出异常)。

与 Job 的差异(最重要):

  • Job.join():只等结束,不给结果;失败通常表现为 CancellationException(CE)。

  • Deferred.await():成功返回 T;失败直接抛出原始异常(不是 CE 封装)。

常用成员(除 Job 的 API 外):

  • suspend fun await(): T
  • fun getCompleted(): T(非挂起,仅在已成功完成时可用,否则抛)
  • fun getCompletionExceptionOrNull(): Throwable?(失败/取消的根因,成功为 null)

2) 结果与异常:装进盒子,await再打开

  • async 中抛出的异常会被封存到 Deferred

    • await() 时抛出;
    • 但如果处于 coroutineScope{} (默认结构化并发),子失败会立刻取消父作用域和兄弟任务,即便你还没 await。
  • supervisorScope{} / SupervisorJob 下,子失败不会影响兄弟/父;异常仍在 Deferred 里,只有 await() 才会抛

口诀:要结果就 await();只等收尾用 join();想“互不影响”用监督(supervisorScope)+ runCatching。


3) 创建方式与组合

3.1async

suspend fun load(): Pair<User, Feed> = coroutineScope {
    val u = async { api.user() }   // Deferred<User>
    val f = async { api.feed() }   // Deferred<Feed>
    u.await() to f.await()
}

3.2CompletableDeferred

  • 手动完成的 Deferred,常用于回调桥接对外暴露只读结果
fun fetchAsDeferred(): Deferred<Response> {
    val d = CompletableDeferred<Response>()
    client.enqueue(object: Callback {
        override fun onSuccess(r: Response) { d.complete(r) }
        override fun onError(e: Throwable) { d.completeExceptionally(e) }
    })
    return d
}

3.3awaitAll / joinAll

val list: List<Deferred<Int>> = tasks.map { async { work(it) } }
val results: List<Int> = awaitAll(*list.toTypedArray()) // 任一失败→抛首错并取消其余

3.4select { deferred.onAwait { … } }

  • 在多个 Deferred/Channel/超时间竞态时选先完成者(需要时再用)。

4) 取消与超时(务必熟悉)

  • 取消 deferred.cancel();收尾用 cancelAndJoin()

  • 被取消后 await() 会抛 CancellationException;根因可用 getCompletionExceptionOrNull() 取到。

  • withTimeout { async/await }:超时抛 TimeoutCancellationException(CE 子类),只取消其作用域内的子任务

适配 CPU 密集型取消(必须协作):

while (isActive) { step() }      // 或定期 check `ensureActive()` / `yield()`

5) Start 模式与LAZY的隐形坑

val a = async(start = LAZY) { slow() }
val b = async(start = LAZY) { slow2() }
// 直接 await 会“串行启动”:先触发 a,再触发 b
a.start(); b.start()             // ✅ 想并发,记得先 start 再 await
val r = a.await() + b.await()
  • DEFAULT:创建即调度/运行(可被优化为 inline 恢复)。
  • LAZY:直到 start/await/join 才启动;多个 LAZY 想并发,务必先 start() 全部
  • UNDISPATCHED:在当前线程执行到首个挂起点再交给目标调度器(减少一次切换)。

6) 常见使用模式

6.1 并发汇总(失败即中断)

suspend fun load() = coroutineScope {
    val a = async { partA() }
    val b = async { partB() }
    combine(a.await(), b.await())
}

6.2 容错并发(不互相拖垮)

suspend fun loadTolerant() = supervisorScope {
    val a = async { runCatching { partA() } }
    val b = async { runCatching { partB() } }
    a.await() to b.await()
}

6.3 只监控不await(避免静默失败)

val d = async { job() }
d.invokeOnCompletion { e -> if (e != null) log("async failed", e) }

6.4 对外只暴露Deferred(读写分离)

private val _once = CompletableDeferred<Config>()
val config: Deferred<Config> get() = _once   // 只读视图

7) Android/服务端落地建议

  • UI 层:viewModelScope.async 做并发加载;在 onCleared() 由 SupervisorJob 取消所有子任务。
  • 不要把 Deferred 保存到跨生命周期的静态/单例里;若确需缓存结果,用 StateFlow/SharedFlow 更合适。
  • 监控异常:对关键 Deferred 加 invokeOnCompletion 上报;或统一 await() 外层 try/catch。
  • 减少切换:Main.immediate / UNDISPATCHED 合理用在首段“轻量逻辑”,重活放 IO/Default。

8) 排错清单(踩坑必看)

  1. 只 join() 不 await() :拿不到原始异常 → 用 await() 或 getCompletionExceptionOrNull()。
  2. GlobalScope.async 不 await:异常静默;不建议这么用。
  3. 以为 async 失败不影响别人:在 coroutineScope 里会立刻取消父与兄弟;需要隔离请用 supervisorScope。
  4. LAZY 导致串行:先 start() 再 await()。
  5. 取消不生效:CPU 循环里要检查 isActive/ensureActive()。
  6. 在锁/临界区里 await:可能长时间持锁甚至死锁;把挂起点搬到锁外。

9) 速记

  • Deferred = Job + 结果await() 拿结果 / 抛原始异常

  • 并发取结果:async + await / awaitAll;

  • 互不影响:supervisorScope + runCatching;

  • 外部完成:CompletableDeferred.complete/completeExceptionally;

  • LAZY 想并发:先 start() 再 await()。