Kotlin 协程 - 任务 Job

发布时间:2026/9/12 21:46:30
Kotlin 协程 - 任务 Job 一、概念对于每一个由协程构建器开启的作用域都会返回一个 Job 对象用来管理该任务的生命周期。launch() 直接返回 Job 实现async() 返回的 Deferred 实现了 Job接口。Jobstart()public fun start(): Boolean若与此任务关联的协程尚未启动则启动它。如果此次调用实际启动了协程则返回 true若协程已启动或已完成则返回 false。cancel()public fun cancel(cause: CancellationException? null)取消 Job 会抛异常默认可空也可以自定义job.cancel(CancellationException(取消))。它不会立马就被取消先进入 cancelling。协程作用域和协程上下文的扩展函数cancel()底层都是调用的它。cancelChildren()public fun Job.cancelChildren(cause: CancellationException? null)只取消子协程。join()public suspend fun join()挂起当前协程直到 Job 完成。cancelAndJoin()public suspend fun Job.cancelAndJoin()挂起当前协程直到 Job 取消完成。invokeOnCompletion()public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandleJob结束后调用该回调不管是cancelled还是competed都会回调。参数 handler 使用了 typealias原始类型是 (cause: Throwable?) - Unit。因此 Lambda 中对 it 进行判断null 为正常完成非 null 为失败或取消当异常类型为CancellationException。joinAll()public suspend fun joinAll(vararg jobs: Job): Unit jobs.forEach { it.join() }挂起当前协程直到传入的 Job 都并行执行完。public suspend fun CollectionJob.joinAll(): Unit forEach { it.join() }挂起当前协程直到集合中的 Job 都并行执行完。Deferredawait()public suspend fun await(): T挂起当前协程直到 Deferred 完成。如果 async { ... }.await() 链式调用来写意义就不大因为启动和取结果没有被真正拆开。async 真正有用的地方是让“开始执行”和“等待结果”不必发生在同一行。分别对两个 async 调用 await()它们是并行执行总耗时最慢执行完的那个。awaitAll()public suspend fun T awaitAll(vararg deferreds: DeferredT): ListT挂起当前协程直到传入的 Deferred 都并行执行完。public suspend fun T CollectionDeferredT.awaitAll(): ListT挂起当前协程直到集合中的 Deferred 都并行执行完。二、生命周期如果Job是通过协程构建器创建的Active就是协程主体运行时的状态在这个状态下我们可以启动子协程。一般协程都是在Active状态下启动只有那些延迟启动的才会以New状态启动。当Job完成时会进入Completing状态等待所有子协程完成然后进入Compelted状态。如果Job在Active或Completing状态下取消或者异常会进入到Cancelling状态供我们做一些资源释放等工作然后进入到Cancelled状态。没有直接的生命周期函数可供调用而是使用以下三个属性去做判断Job的状态/通过属性判断.isActive.isCompleted.isCancelledNew 创建时optional initial statefalsefalsefalseActive 执行中default initial statetruefalsefalseCompleting 完成前transient statetruefalsefalseCancelling 取消中transient statefalsefalsetrueCancelled 已取消final statefalsetruetrueCompeleted 已完成final statefalsetruefalsesuspend fun Doas() coroutineScope { while (isActive) {...} launch { if (isActive) {...} } }三、协程的取消Java 线程其实没有提供任何机制来安全地终止线程Thread 类提供了一个 interrupt() 方法用于中断线程的执行并不意味着立即停止目标线程正在进行的工作而只是传递了请求中断的消息然后由线程在下一个合适的时机中断自己仅仅终止线程是一个糟糕的方案。协程提供了一个 cancel() 函数来取消Job但并不一定会取消只是将状态 isActive 标记为 false。协程的取消是协作的一段协程代码必须协作才能被取消。所有 kotlinx.coroutines 中内置的挂起函数都是 suspendCancelableCoroutine 可取消类型这些挂起点在执行时都会检查协程的取消并在取消时抛出 CancellationException 停掉后面代码的执行对挂起函数进行 try-catch 会导致 Job 取消失败引发内存泄漏或逻辑错误必须在 catch 块中对 CancellationException 类型进行资源释放并再次抛出详见协程的异常处理。因此只有当执行到下一个挂起点任务才会被取消如果自定义的挂起函数是通过不支持取消类型的 suspendCoroutine() 包装的需要手动检查状态并处理否则代码会一直执行到该Job结束。但也有例外见下文。fun main() runBlocking { val job1 launch { println(1开始) Thread.sleep(2000) //线程睡不会检查取消 println(1结束) } val job2 launch { println(2开始) delay(2000) //协程睡是挂起函数会检查取消 println(2结束) } delay(1000) job1.cancel() //打印1开始、1结束 job2.cancel() //打印2开始 }runCatching { //捕获挂起函数就需要过滤并抛出 CancellationException }.onFailure { if (it is CancellationException) { throw it } } try { //捕获挂起函数就需要过滤并抛出 CancellationException } catch (e: Exception) { if (e is CancellationException) { throw e } } suspend fun demo() coroutineScope() { try { //捕获业务代码不影响挂起函数检查取消抛出 CancellationException } catch (e: Exception) {} }3.1 原理public interface Job : CoroutineContext.Element { //通过序列保存了所有子Job的引用所以具有父子层级结构 public val children: SequenceJob } //子Job接口 public interface ChildJob : Job { //提供了父Job取消自己的函数 public fun parentCancelled(parentJob: ParentJob) } //父Job接口 public interface ParentJob : Job { //提供了获取子Job被取消原因的函数 public fun getChildJobCancellationCause(): CancellationException } //Job的实现类同时实现了ChildJob和ParentJob说明一个Job对象既可以是父Job也可以是子Job public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob, SelectClause0 { internal var parentHandle: ChildHandle? //当前协程的父协程帮助类当前协程作为子协程 }3.2 过程suspendCancelableCoroutine类型的挂起函数挂起点会在执行的时候检查协程的状态其它情况需要手动调用 job.isAlive 或 job.ensureActive() 来检查。运行出错或者调用cancel()后该Job会在遇到第一个挂起点开始取消并抛出CancellationException异常先处于Cancelling状态没有挂起点或检查措施便不会响应取消操作直至代码块执行完毕。手动调用join()或遇到代码中的第一个挂起点协程才会真正被取消再处于Cancelled状态。推荐使用cancelAndJoin()简化调用。一旦该Job被取消该Job下的子job也会一并取消但父Job和兄弟Job不受影响该Job不能再用作任何新Job的父Job不能开启新协程。3.3 取消的异常处理协程通过抛出一个 CancellationException 异常来取消 Job。cancel() 可以传参使用不同的异常来指定原因需要是 CancellationException 的子类才能取消协程。该异常不会导致父协程或兄弟协程的取消在 cancelParent 之前会被过滤掉不会传递给父Job可以使用 try-catch-finally 去捕获处理释放资源推荐使用标准函数 use() 会自动关闭资源。suspend fun main() runBlocking { //没有继承父协程的上下文有自己的作用域因此 runBlocking 不会等待 GlobalScope 执行完再结束。 val job GlobalScope.launch { try { //耗时操作 }catch (e:Exception){ //处理异常 }finally{ //释放资源 } } delay(1000) //让job运行一下再取消 // job.cancel() //抛异常 JobCancellationException // job.join() //挂起函数这样就会等 GlobalScope 取消完再继续执行 job.cancelAndJoin() //简写 }3.4 无法直接取消的情况CPU密集型、不支持挂起的IO操作由于调用cancel()操作后Job会处于Cancelling状态此时只需判断Job是否处于活跃状态于便可以响应cancel()操作。CPU密集型任务无法直接被cancel()取消因为直接取消会丢失临时计算数据。可以通过对Job状态的判断来响应cancel()操作。Job的取消发生在挂起点上没有挂起点便不会响应cancel()操作继续执行直到把活干完。当我们使用协程却没有调用任何挂起函数的时候如使用不支持挂起的Java阻塞操作API便会发生这种情况。isActive加在判断里public val CoroutineScope.isActive: Booleanget() coroutineContext[Job]?.isActive ?: true判断Job是否处于活跃状态尚未完成且尚未取消。ensureActive()写在函数里public fun CoroutineScope.ensureActive(): Unit coroutineContext.ensureActive()返回coroutineContext扩展函数调用Job的函数最终调用的是 !isActiveJob处于非活跃状态就报错CancelllationException。yield()不至于抢占太多线程让其它协程拿不到执行权public suspend fun yield(): Unit会检查所在协程的状态如果已经取消则报错 CancellationException此外会尝试让出线程执行权。runInterruptible()替代 withContext() 切换线程public suspend fun T runInterruptible(context: CoroutineContext EmptyCoroutineContext,block: () - T): T底层通过只负责切换线程的 withContext() 实现会检查 Job 状态然后调用 Thread.interrupt() 去释放线程。fun main() runBlocking { val job launch(Dispatchers.Default) { //该协程中无挂起点同理自定义的挂起函数内部无挂起点 while (isActive) { //判断出false便会取消 ensureActive() //检测出false便会取消 yield() //不至于因为任务太抢占资源导致其它协程拿不到线程执行权 println(CPU密集任务/不支持挂起的IO操作) } } delay(1000) //让job运行一会儿后再取消 println(等完) job.cancelAndJoin() //cancel()操作会将 isActive false println(结束) }suspend fun readFile() runInterruptible(Dispatchers.IO) { Thread.sleep(5000) // 假设读取很慢 File(data.txt).readText() }3.5 回调由于我们可以捕获CancellationException异常在 Job 真正结束前可以做一些事情由于 Job 响应 cancel() 后已经处于 Cancelling状态此时启动一个新协程会被忽略或者调用挂起函数会抛异常CancellationException是无法被执行的。方式①指定协程上下文为NonCancellable来得到一个常驻Job不响应 cancel()操作。方式②使用invokeOnCompletion()函数当 Job 处于Cancelled状态或Compeleted状态时会执行回调。形参it是一个异常没有异常值为null协程被取消值为 CancellationException。withContext(NonCancellable) { //不会响应取消 } job.invokeOnCompletion { //回调代码 }3.6 自定义挂起函数的处理详见回调函数改挂起函数//定义 suspend fun getResource():StudentBean suspendCancellableCoroutine{ continuation - request(object : ICallBack{ override fun onSuccess(data:String){ continuation.resume(data) } override fun onFailure(exception:Throwable){ continuation.resumeWithException(exception) } }) //定义协程取消时应该做的操作 continuation.invokeOnCancellation{ //TODO... } } //使用 fun main() runBlocking{ scope.launch{ val bean getResource() } }四、自定义Job协程构建器基于其父Job构建自己的Job如果自定义了Job便不再适用父子关系失去了结构化并发父协程的取消不会连带取消该子协程该子协程的异常不会向上传播。是一个独立的子作用域它是根协程其中开启的子协程是顶级协程它必须自己处理异常而不会像 coroutineScope() 那样抛出。fun main(): Unit runBlocking { val scope CoroutineScope(Job()) // test1(scope) //打印测试1---子协程1、异常 test2(scope) //打印测试2---子协程1 、异常、测试2---子协程2 delay(1000) } fun test1(scope: CoroutineScope) { scope.launch { launch { //此处基于父协程构建会影响兄弟协程 println(测试1---子协程1) throw Exception() } launch { delay(1000) println(测试1---子协程2) //不会打印 } } } fun test2(scope: CoroutineScope) { scope.launch { launch(Job()) { //此处自定义了job是独立的不会影响兄弟协程 println(测试2---子协程1) throw Exception() } launch { delay(1000) println(测试2---子协程2) //会打印 } } }

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询