【深入理解Kotlin协程】使用Job控制协程的生命周期

news2024/10/3 4:44:02
Job 是协程上下文CoroutineContext的实现之一,通过它我们可以对协程的生命周期进行一些控制操作。
            
Job 是协程的 句柄。使用  launch 或  async 创建的每个协程都会返回一个  Job 实例对象,该实例是相应协程的唯一标识并管理其生命周期。还可以将  Job 传递给  CoroutineScope 以进一步管理其生命周期,如以下示例所示:
class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }
        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

我们通过Job可以获取当前协程的运行状态,还可以随时取消协程

协程的状态查询:
  • isActive 活跃
  • isCompleted 已完成
  • isCancelled 已取消
如果协程处于活跃状态,协程运行出错或者调用  job.cancel() 都会将当前任务置为取消状态 ( isActive = false, isCancelled = true)。当所有的子协程都完成后,协程会进入已完成状态,此时  isCompleted = true
    
常用的协程操作:
  • cancel 用于Job的取消,取消协程
  • start 用于启动一个协程,让其到达 Active状态
  • invokeOnCompletion 添加一个监听,当工作完成或者异常时会调用
  • join 阻塞并等候当前协程完成

start

协程不是默认创建就启动了吗? 怎么还有一个 start 方法 。
  • 这个方法主要是针对通过懒加载 Lazy模式创建的协程,需要进行手动 start才能启动协程。
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
    println("执行在协程中...") 
    delay(1000L) 
    println("执行完毕...") 
}
job.start()

cancel

协程的取消,我们之前也讲到过,一般我们可以手动调用  cancel 或者在 onDestory的时候调用  cancel:
var job = GlobalScope.launch { 
    println("执行在协程中...") 
    delay(1000L)
    println("执行完毕...") 
} 
...
override fun onDestroy() { 
    job.cancel()
    super.onDestroy() 
}

invokeOnCompletion

协程执行完的回调  invokeOnCompletion 也是我们常用的监听,在正常执行完毕,或者异常执行完毕都会回调这个方法。
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> 
    YYLogUtils.e(throwable.message ?: "Unkown Error") 
} 
val job = GlobalScope.launch(Dispatchers.Main + exceptionHandler) { 
    println("执行在另一个协程中...") 
    delay(1000L) 
    val num = 9/0 
    println("另一个协程执行完毕...") 
} 
job.invokeOnCompletion { 
    println("完成或异常的回调") 
}
没有异常的回调:

 加入9/0的异常代码之后的回调:

join

join()方法会暂停所处的协程,直到Job中的代码执行完毕再继续,一般用来等待某个协程执行完毕,它是一个挂起函数。(跟 Deferred.await()方法的调用有异曲同工之处, await() 会等待  async{ } 中的代码返回结果后再继续。)
fun main() = runBlocking {
    val job = launch {
        delay(100)
        println("hello")
        delay(300)
        println("world")
    }
    println("test1")
    job.join()
    println("test2")
}
输出:

cancelAndJoin

suspend fun main() { 
    runBlocking {
        val job = launch {
            repeat(1000) { i ->
                println("job: test $i ...")
                delay(500L)
            }
        }
        delay(1300L) // 延迟一段时间
        println("main: job.cancel() called!")
        // job.cancel() // 取消该作业
        // job.join() // 等待作业执行结束
        job.cancelAndJoin() // 等价以上两行,取消一个作业并等待它结束
        println("main: job is canceled.")
    }
}
输出:
 
问题: 如果先调用 job.join() 后调用 job.cancel() 会是什么情况?
  • 协程中的代码不会被取消,会全部执行完。

协程的取消是协作的

协程并不是一定能被取消的,协程的取消是协作的一段协程代码必须协作才能被取消所有 kotlinx.coroutines 中的挂起函数都是可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException
          
如果协程正在执行不可中断的计算任务,并且没有检查取消的话,那么它是不能被取消的。这一点上跟Java的线程取消有点类似。
suspend fun main() {
    runBlocking {
        val job = launch(Dispatchers.Default) {
            var nextPrintTime = System.currentTimeMillis()
            var i = 0
            while (i < 100) { // 一个执行计算的循环,只是为了占用 CPU
                // 每秒打印消息两次
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: hello ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }
        delay(1300L) // 等待一段时间
        println("main: job.cancel() called!")
        job.cancelAndJoin() // 取消一个作业并且等待它结束
        println("main: Now job is canceled.")
    }
}

输出:

 

 可以看到上面代码中当调用 job.cancelAndJoin() 之后,协程并没有被立即取消掉,而是继续执行完while循环所有的计算工作后才自动停止。也就是说上面代码跟先调用 job.join() 后调用 job.cancel() 的效果是一样的。

检查 Job 状态

解决上面代码问题的第一种办法是修改 while循环的判断条件,添加对 Job状态的检查,例如 isActive或isCancelled
while (i < 100 && isActive)
while (i < 100 && coroutineContext[Job]?.isActive == true)
while (i < 100 && coroutineContext[Job]?.isCancelled == false)

以上三种写法都可以。

另一种方法使用协程标准库中的函数  ensureActive() :
while (i < 100) {
    ensureActive()
    ...
}

ensureActive() 的实现是这样的:

public fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}

其实跟第一种原理是一样的,也是调用了isActive来判断的。

第三种办法是 使用  yield() 支持可响应取消 :
while (i < 100) {
    yield()
    ...
}

yield()方法是一个可取消的挂起函数,它的作用是:如果当前协程的Job在调用此挂起函数时被取消或完成,或者在此函数等待分派时,它将使用CancellationException作为结果来恢复协程。因此它可保证立即取消协程 yield() 应该在定时检查中最先被调用。 

检查协程是否为已取消状态,在业务开发中是非常有必要的,例如,如果您要从磁盘读取多个文件,请先检查协程是否已取消,然后再开始读取每个文件。
someScope.launch {
    for(file in files) {
        ensureActive() // Check for cancellation
        readFile(file)
    }
}

Deferred(带结果的Job)

Deferred 是  Job 的子类,它是一种可以返回结果的 Job,可以类比 Java 中的  Future 对象,在 Java 中  Future.get() 可以等待异步线程任务的执行结果,而  Deferred.await() 主要用来等待  async{ } 启动的协程代码块的返回结果:
fun main() = runBlocking {
    val deferred = async {
        ...
        delay(5000)
        10
    }
    val result = deferred.await() 
}
deferred 也是可以取消的,对于已经取消的 deferred 调用 await() 方法,会抛出  JobCancellationException 异常。
   
同理,在 deferred.await()  之后调用 deferred.cancel(),  那么什么都不会发生,因为任务已经结束了。
     
async 可以通过将  start 参数设置为  CoroutineStart.LAZY 变成惰性的。在这个模式下,调用  await 获取结果时,最好调用  Job 的  start 方法启动协程。
fun main() { 
    runBlocking {
        val time = measureTimeMillis {
            val deferred1 = async(Dispatchers.IO, CoroutineStart.LAZY) {
                printInfo()
                delay(1000) // 模拟耗时操作
                1
            }
            val deferred2 = async(Dispatchers.IO, CoroutineStart.LAZY) {
                printInfo()
                delay(3000) // 模拟耗时操作
                2
            }
            deferred1.start()
            deferred2.start()
            printInfo("${deferred1.await() + deferred2.await()}")
            printInfo("end")
        }
        printInfo("time: $time")
    }
}

fun printInfo(msg : String? = null) {
    println("${Thread.currentThread().name}: ------> ${msg ?: ""}")
}

输出:

问题:如果不调用 start() 情况会怎样?

上面代码如果不调用 deferred1.start() deferred2.start() 方法,输出如下:
 
虽然也正确输出结果了,但是会发现 总耗时是两个async块中耗时的累加
   
也就是说调用 start之后两个 async块中的代码是 并发执行的,所以最终耗时是二者中耗时最长的那个任务的时间。如果不调用 start,则由于 LAZY启动模式的特点,只有在被需要的时候才会去启动这个协程,也就是说当执行到 deferred1.await() 时,才会去执行第一个 async{} 的代码,等这个执行完毕后,再继续执行  deferred2.await() 时,才会去执行第二个 async{} 的代码,所以最终耗时是二者的累加。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7292.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构与算法(四) 广度优先搜索

本篇文章继续来学习广度优先搜索算法&#xff08;Broad-First-Search&#xff0c;BFS&#xff09; 1、本质 广度优先搜索本质上还是遍历整个搜索空间&#xff0c;找到给定问题的解 实际上也是一种暴力搜索算法&#xff0c;不过其中的实现细节和优化细节还是值得探讨的 与深度…

基于PHP+MySQL的大学生交友社交网站

近年来,大学生的数量在逐步的增加,为了能够让这些大学生有一个更好的交友环境,需要创建一个基于大学生的社交交友网站。这样可以拉近彼此大学生之间的感情,让他们可以更好的进行学习和交流。 PHP大学生交友社交网站通过PHP&#xff1a;MySQL进行开发,分为前台和后台两部分,通过…

线程的状态

Java中线程的状态是通过枚举类型Thread.State表示的 &#xff0c;通过打印这些枚举类型&#xff0c;就可以知道java中线程的状态有哪些 public class ThreadState {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.pr…

进销存管理系统是什么?有哪些功能?

对于2022年刚开始&#xff0c;但是可以的确的是禽流感在短时期内是不可能返回他们&#xff0c;作为虚拟店面批发商想勇往直前中&#xff0c;就必须要亲吻网络&#xff0c;把销售业务从实体店搬至线上去。 想突破现状&#xff0c;化解虚拟店面批发民营企业的存活问题&#xff0…

ES初使用记录——写入与查询数据

本周接到一个任务&#xff1a;定时统计订单表中的数据&#xff0c;将异常订单挑出来&#xff0c;放入ES中供统计页面从总数点击跳转过去进行列表展示。 一、配置ES 配置maven&#xff0c;注入template Resource private ElasticsearchTemplate elasticsearchTemplate; 二、…

一、OBS概述

1. 概述 obs官网git源码编译 2. 软件能力 输入&#xff1a;文本、图片、窗口、音视频及摄像头等 处理&#xff1a;视频及图片滤镜、音频混音等 输出&#xff1a;rtmp推流、本地录制(mp4)、音视频裸数据(pcm/yuv/rgb)等 3. 模块 a. core核心模块 libobs 加载、管理各个功能…

Ansys Zemax | 大功率激光系统的STOP分析2:如何进行光机械设计准备

大功率激光器广泛用于各种领域当中&#xff0c;例如激光切割、焊接、钻孔等应用中。由于镜头材料的体吸收或表面膜层带来的吸收效应&#xff0c;将导致在光学系统中由于激光能量吸收所产生的影响也显而易见&#xff0c;大功率激光器系统带来的激光能量加热会降低此类光学系统的…

性能测试面试题总结(答案全)

目录 1.什么是负载测试&#xff1f;什么是性能测试&#xff1f; 2.性能测试包含了哪些测试&#xff08;至少举出3种&#xff09; 3.简述性能测试的步骤 4.什么时候可以开始执行性能测试&#xff1f; 5.你如何在负载测试模式下执行功能测试&#xff1f; 6.响应时间和吞吐量…

时序数据库 InfluxDB

一、介绍 InfluxDB 是一个时间序列数据库&#xff0c;GO 编写的,旨在处理高写入和查询负载。InfluxDB 旨在用作涉及大量时间戳数据的任何用例的后备存储&#xff0c;包括 DevOps 监控、应用程序指标、物联网传感器数据和实时分析。 特点&#xff1a; 专门为时间序列数据编写的…

格式工厂安装与使用教程

格式工厂支持各种类型视频、音频、图片、word转pdf等多种格式的免费转换&#xff0c;是一款非常优秀的良心软件。 在电脑浏览器中打开下载地址http://www.pcgeshi.com/index.html , 单击"立即下载"按钮即可。 打开下载的文件&#xff0c;等待安装即可。&#xff08…

标记肽Bz-Pro-Phe-Arg-pNA、59188-28-2

血浆激肽释放酶&#xff0c;cruppain和胰蛋白酶的显色底物。编号: 140214 中文名称: 标记肽Bz-PFR-对硝基苯胺 英文名: Bz-Pro-Phe-Arg-pNA CAS号: 59188-28-2 单字母: Bz-PFR-pNA 三字母: Benzoyl-Pro-Phe-Arg-pNA 氨基酸个数: 3 分子式: C33H38O6N8 平均分子量: 642.7 精确分…

python 多线程编程(线程同步和守护线程)

守护线程&#xff1a; 随着主线程的终止而终止&#xff0c;不管当前主线程下有多少子线程没有执行完毕&#xff0c;都会终止。 线程同步&#xff1a; join所完成的工作就是线程同步&#xff0c;即主线程任务结束之后&#xff0c;进入阻塞状态&#xff0c;一直等待其他的子线程执…

深入了解tomcat线程池

1.概述 在正式进入Tomcat线程池之前&#xff0c;小伙伴们可以先回顾一下JDK中的线程池相关特性&#xff0c;对于JDK线程池的总结和源码的解析感兴趣的童鞋&#xff0c;也可参考博主的层层剖析线程池源码的这篇文章&#xff0c;文章主要讲述对线程池的生命周期&#xff0c;核心参…

Vue3 - 不再支持 IE11,到底为什么?

前言 咱们的 Vue2 目前仍然支持 IE11&#xff0c;但是到了 Vue3 这里&#xff0c;直接被抛弃了。 IE 浏览器可以说是早期前端开发的噩梦&#xff0c;现在还充斥的大量兼容 IE 浏览器的代码&#xff0c;你可以在网上看到很多类似的信息。 IE 浏览器下 float 布局错乱。IE 浏览器…

商务呈现之沟通管理-上

一、前言 课程目标及适用人群课程目标:商务/项目的目标达成,任务推动,良好的商务呈现 现实的困扰我们商务活动中是否有遇到以下情况: (1)需求老是变 理解不一致细节不清晰(2)CR很难谈 需求基线不清晰没有利用好"交换"(3)原地打转 事项推进缓慢几个月还在讨…

相似度系列-6:单维度方法:Evaluating Coherence in Dialogue Systems using Entailment

Evaluating Coherence in Dialogue Systems using Entailment coherence 英文中意味着连贯性、条理性。 这篇文章是面向对话应用的&#xff0c;更加关注于对话中上下位的连贯性。1. 直接转换为 NLI问题&#xff0c;premise-hypothesis问题。——2. 数据集是自己构造的。——数…

一文带你了解【抽象类和接口】

1. 抽象类概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是并不是所有类都是用来描绘对象的。如果一个类中没有包含足够的信息来描绘一个具体的对象&#xff0c;这样的类就是抽象类。 举个简单的例子 上图中&#xff0c;三角形&#xff0…

构建一个商业智能 BI 分析平台,公司CIO应该重点关注什么?

企业级商业智能 BI 分析平台的构建是一个系统型的工程&#xff0c;涉及业务分析需求的把控、各类数据资源的整合清洗、数据仓库的架构设计、可视化分析报表逻辑设计、IT 部门与业务部门的工作边界划分与配合等等居多环节。 每一个环节的重要性都不容忽视&#xff0c;第一是业务…

(算法设计与分析)第三章动态规划-第二节:动态规划之背包类型问题

文章目录一&#xff1a;01背包问题&#xff08;1&#xff09;题目描述&#xff08;2&#xff09;解题思路&#xff08;3&#xff09;完整代码二&#xff1a;分割等和子集&#xff08;01背包变形&#xff09;&#xff08;1&#xff09;题目描述&#xff08;2&#xff09;解题思路…

Java:Jar包反编译,解压和压缩

1、简述 JAR 文件就是 Java Archive &#xff08; Java 档案文件&#xff09;&#xff0c;它是 Java 的一种文档格式。 JAR 文件非常类似 ZIP 文件。准确的说&#xff0c;它就是 ZIP 文件&#xff0c;所以叫它文件包。JAR 文件与 ZIP 文件唯一的区别就是在 JAR 文件的内容中&a…