协程中的Job

64 阅读4分钟

Job 是什么?

  • Job = 协程的生命周期句柄:表示一段协程工作是否开始、是否还在运行、是否已取消/完成。

  • 作用:管理与观测(cancel / join / isActive / invokeOnCompletion)、形成父子层级(结构化并发)、承载取消原因

取当前协程的 Job:val job = coroutineContext[Job]!!

生命周期状态机(简化)

New (LAZY) → Active → (Completing) → Completed(正常)
                 │         └→ Cancelling → Cancelled(带 cause)
                 └→ Cancelling → Cancelled
  • New:仅 CoroutineStart.LAZY 才会出现(还未启动)。

  • Active:已启动在跑;job.isActive == true。

  • Completing:主体已结束,等待子协程/回调收尾

  • Cancelling/Cancelled:进入取消流程 / 最终取消。

  • Completed:正常完成。

父 Job 直到所有子 Job 完成才会进入最终 Completed/Cancelled(结构化并发保证“父等子”)。

父子传播规则(结构化并发)

  • 子失败 ⇒ 取消父与兄弟(在 coroutineScope/普通 Job 层级里)。
  • 父取消 ⇒ 取消所有子(同一个 CancellationException 原因向下传播)。
  • 监督模型:用 supervisorScope 或 SupervisorJob,使“子失败不影响兄弟/父”。
// 普通父子:一死全死
coroutineScope {
    val a = launch { … }
    val b = launch { error("boom") } // 立刻取消 a 与父
}

// 监督:各玩各的
supervisorScope {
    val a = launch { … }
    val b = launch { error("boom") } // a 不受影响
}

取消与完成(异常模型)

  • 取消 API:job.cancel(cause: CancellationException? = null),或 cancelAndJoin()(取消后挂起等待结束)。

  • 取消本质:在下一个挂起点注入 CancellationException,触发你代码中的 try/finally 收尾。

  • 获取原因

    • job.getCancellationException():取消必有 CancellationException。
    • job.getCompletionExceptionOrNull():失败或取消的根因(正常完成为 null)。
  • 完成回调:job.invokeOnCompletion { cause -> … }

    • cause == null 表示正常完成;否则是取消/失败的异常。

    • 回调在线程语义:在哪个线程让它完成,就在哪个线程回调(不要在里头做重活)。

在 finally 里需要保证收尾不被取消:withContext(NonCancellable) { … }

Job vs Deferred(有无“结果”的区别)

  • Job:只有生命周期,没有返回值;join() 只等待,不抛出原始异常(最多 CancellationException)。

  • Deferred :Job 的子类型,带结果;await() 返回 T 或直接抛出原始异常

  • 经验:要结果用 async/await(Deferred),只做事用 launch/Job

创建与组合

  • launch { … } → Job

  • async { … } → Deferred (也是 Job)

  • Job() / SupervisorJob() → 根 Job(CompletableJob) ,可作为 CoroutineScope(context + job) 的锚点。

  • joinAll(vararg jobs) / awaitAll(vararg deferreds):批量等待;awaitAll 任一失败会抛首个异常并取消其余。

Start 模式(与 Job 的关系)

  • DEFAULT:马上调度/运行。

  • LAZY:创建为 New,直到 start/join/await 或首次需要时才启动。

  • ATOMIC:启动到第一个挂起点前不可取消(避免“刚起就被取消”)。

  • UNDISPATCHED:当前线程一路执行到首个挂起点,再交给调度器(少一次切换)。

常用 API 速记

  • 观测:isActive / isCompleted / isCancelled

  • 等待:join()、cancelAndJoin()

  • 取消:cancel(cause)、cancelChildren()

  • 回调:invokeOnCompletion(onCancelling = false, invokeImmediately = false) { cause -> }

  • 层级:children(遍历子协程)

  • 诊断:getCompletionExceptionOrNull()、CoroutineName 标注日志

Android/后端实战范式

1) 防抖/换源:先取消旧 Job

private var searchJob: Job? = null

fun onQuery(q: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        val data = repo.search(q)   // suspend
        _state.value = data
    }
}

2) 并发加载 + 一死全死(默认结构化)

suspend fun load(): Pair<User, Feed> = coroutineScope {
    val u = async { api.user() }
    val f = async { api.feed() }
    u.await() to f.await()         // 任一抛错,取消另一半与父
}

3) 并发容错(监督 + Result)

suspend fun loadTolerant(): Pair<Result<User>, Result<Feed>> = supervisorScope {
    val u = async { runCatching { api.user() } }
    val f = async { runCatching { api.feed() } }
    u.await() to f.await()
}

4) 在 finally 确保收尾

launch(Dispatchers.IO) {
    val handle = open()
    try {
        work()
    } finally {
        withContext(NonCancellable) { handle.close() }
    }
}

5) 自定义根 Job 绑定组件生命周期

class MyComponent {
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    fun destroy() { job.cancel() } // 取消全部子协程
}

排错与坑

  1. 只 join() 不见异常:join() 不抛原始异常;用 Deferred.await() 或 getCompletionExceptionOrNull()。

  2. 忘了“父等子” :主体结束但子协程还在,父 Job 不会 Completed;确保子协程收尾或 cancelChildren()。

  3. 在锁内挂起:持锁时取消/超时会导致长时间占锁甚至死锁;把挂起点移出临界区或用无阻塞结构。

  4. 回调线程误用:invokeOnCompletion 回调运行在线程不确定处,避免操作 UI 或重工作业。

  5. GlobalScope.async 不 await:异常“吞掉”;要么 await(),要么 invokeOnCompletion 做监控;更推荐结构化 scope。

  6. 取消不生效:CPU 密集循环里需协作取消:ensureActive() / yield() 或定期检查 isActive。

  7. 超时理解:withTimeout 抛 TimeoutCancellationException(CancellationException 子类),只取消子协程;如果没捕获,会按父子规则上抛。

一句话总结

  • Job 管“生老病死”,Deferred 额外管“结果”。****
  • 默认结构化并发:子失败会“上行取消父、横向取消兄弟”;监督模型可隔离失败。
  • 学会三板斧:cancelAndJoin 收尾invokeOnCompletion 监控NonCancellable 保证清理