android kotlin 协程(三) 理解挂起,恢复以及job

news2025/2/26 5:57:45

android kotlin 协程(三) 理解挂起,恢复以及job

前言: 通过上两篇的基础入门,相信大家对协程api已经有了一个基本的影响,本篇开始尝试理解挂起于恢复.

本篇不涉及源码, 通过很多应用案例,来理解挂起于恢复!

协程执行流程理解

还是老套路,先来看原始效果

参考图1

image-20230214104006841

在这里我们知道runBlocking会阻塞主线程来等待子协程的执行, 我们通过了kotlin提供的API measureTimeMillis() 来计算出runblocking的执行时间

**kotlin协程本质就是一个线程框架,**我们在使用线程的时候, 通过调用Thread#start() 来通知JVM我们需要执行一个任务

然后JVM去调度执行

thread状态参考

协程也是类似的,在协程中也有调度的概念

在第一篇中我们提到过, 只要是suspend函数,一定是异步的,除非自己想不开调用Thread.sleep,这也是学习协程我认为最重要的一点

首先我们要知道launch{} 中会做那些事情, 通过CoroutineScope#launch{} 创建一个协程的时候, 首先会进入到调度前准备阶段,也就是开启一个协程

开启一个协程的时候并不是执行阶段,只会将这个协程标记为活跃状态,此时这种状态是不会执行协程体中的代码

先有调度, 在有执行

参考图2

image-20230214133713949

  • Job#isActivte 协程是否活跃
  • Job#isCancelled 协程是否被取消
  • Job#isCompleated 协程是否执行完成协程体

因为协程需要调度, 并且这段代码是运行到main线程的,所以这段代码默认会执行调度前的代码,然后才是执行**调度后(执行阶段)**的代码,

这段代码是运行在Main线程中的,如果我们将他放入IO线程,看看会有什么变化

参考图3

image-20230214134901984

可以看出,Main线程 和 IO线程打印出来的执行顺序是不一样的,

这一点也好理解, 因为子协程会跟随父协程的线程,所以子协程中打印就会有时快有时慢

这取决与你CPU的运行速度

但是有一点,即使是IO线程,也会有调度的说法

这里有细心的朋友也可能会看到:

按照上面的说法,在这里代码3先执行,然后在执行的代码5,那么为什么此时Job还是活跃,未完成状态呢?

这是因为他虽然执行完了代码块中的代码,但是还是挂起状态,直到恢复之后才是完成状态!

有了这个前提,接下来步入到本篇的核心, 理解 挂起于恢复

我没有学习协程之前和学一段时间协程后看到挂起与恢复这两个字也很陌生

因为我们知道,挂起是通过suspend关键字 来标记的,但是恢复,怎么恢复? 谁来恢复?

这里先剧透一下后面的内容,不深究,先有个概念就好

我们都知道kotlin是可以反编译为java代码的,但是java中并没有suspend关键字,

那么我们要在java中调用kotlin中的suspend函数,就会要传入一个Continuation的东西, 其本质就是一个回调

简单的说就是 : suspend关键字 就是 Continuation

如果你看过Continuation这个类就知道, 恢复其实指的就是 Continuation#resumeWith

因为我们并没有Continuation 对象,我们只有suspend关键字,所以恢复的代码全部都是kotlin帮我们完成的

恢复其本质就是kotlin帮我们调用 Continuation#resumeWith

反编译看看:

image-20230214142817593

好了,这里就不展开讲了,后面在聊这些,直到这行代码,你只要清楚,恢复的工作,是不需要我们的,是kotlin帮我们完成的

就像是开自动挡车一样,不需要踩离合,并且也没有离合

接下来看一些案例,来更好的理解挂起于恢复, 我们都知道 delay() 是挂起函数,那么我们通过比较 Thread.sleep 与 delay 的区别来理解 挂起于恢复

案例1

sleepdelay
image-20230214145021796image-20230214145050593

sleep()代码应该比较好理解, 就是正常代码阻塞,我不执行完,你不准执行

就好像是地铁安检一样, 如果前面的那个人身上有危险物品,那么安检员就一直等待着,直到排除这个人没问题之后再放行

delay()则不同,delay是一个挂起函数, 当他挂起的时候,会恢复现在已经挂起的函数,也就是会执行 代码3中的代码

这也就是我为什么说suspend函数中的代码永远是异步的

delay()中的代码很简单,也很重要,对理解挂起于恢复有很大的帮助,建议反复琢磨!

案例2

再来看一个suspend不会阻塞的代码

sleepdelay
阻塞:用时3s非阻塞:用时2s
image-20230214151815200image-20230214151727818

sleep就不用过多介绍了,还是按顺序执行,并不会有任何变化

delay(): 这里需要注意的是 代码4代码3执行的更快! 并且总耗时为2s, 这就证明了两个协程之前并不会有关联,他们都是异步执行的,虽然在同一个Main线程内!

说白点就是: 在Main线程内异步执行代码!

如果是线程,要写这样的功能该怎么写呢?

先thread 然后执行完异步任务之后在通过handler切换到main线程

案例3

接下来,就不看结果了, 你能正确打印出他的执行流程嘛?

fun main() {
    println("main start")  // 1

    runBlocking<Unit> {
        println("runBlocking start")  // 2
        delay(2000)
        println("runBlocking end") // 3
    }

    println("main 执行中..") // 4

    Thread.sleep(2100)
    println("main end") // 5
}

| |

| |

| 占位符 |

| |

| |

这段代码比较简单,我们从第一篇就开始说, runBlocking 会阻塞主线程来等待自己子协程执行,这里没有子协程,只有一个delay,所以会等待delay执行完再往下执行

所以这里的顺序为

1,2,3,4,5

案例4

fun main() = runBlocking<Unit> {

    println("main start") // 1

    val job = launch {     // 协程1
        println("launch 1 start") // 2
        delay(1000L)  // TODO 延时1
        println("launch 1 end")  // 3
    }

    println("main mid")  // 4

    val job2 = launch {   // 协程2
        println("launch 2 start") //5
        delay(1000L) // TODO 延时2
        println("launch 2 end") //6
    }

    delay(1500) // TODO 延时3
    println("main end") // 7
}

| |

| |

| 占位符 |

| |

| |

这里难度稍微升级了,在这块代码中,一定是先执行调度前的代码

也就是 1, 4 和 延时3

因为延时3会恢复正在挂起的函数执行

所以会执行 协程1协程2中的代码

那么首先肯定先执行 2

敲黑板了,协程始终是异步的,所以这里执行完 2,就会执行 5

然后同时等待1秒

那么正确结果为:

1,4,2,5,3,6,7

案例5

image-20230214155845205

| |

| |

| 占位符 |

| |

| |

这里理解的是Job状态的掌握, 你可以打印一下Job的三种状态[isActive,isCancelled,isCompleted]

首先执行调度前的任务执行 1,4

这始终都是不变的

但是执行到 **job.join()**的时候,会等待协程1执行完,那么就会执行协程1中的代码

现在的顺序为1,4,2,3

此时,job的状态为 isActive = false, isCompleted = true

说明这个job已经执行完了,这个job生命已经结束了

那么将一个没有生命的job给到一个新的协程中,运行起来可想而知,这个协程也是没有生命的

所以协程2中的代码不会执行

最终正确结果为:

1,4,2,3,7

案例6

image-20230214164843390

| |

| |

| 占位符 |

| |

| |

在这段代码中,重点就是协程2使用了 父协程的coroutineContext 与 自己的DIspatchers.IO

由案例5知道,

在join之前的代码执行结果都一样

1,4,2,3

然后最开始执行协程2,在这段代码中,因为我们是用了自己的Dispatchers.IO,所以开启的了一个IO子协程

最重要的是coroutineContext[Job] 是什么状态

因为这个Job是在coroutineScope内部使用的,很明显,

Job的状态 isActive = true, isCompleted = false, isCancelled = false

然后会通过阻塞2阻塞线程2s

这里的关键点:

  • coroutineContext[Job] 存活 可以执行协程2,中协程体的代码
  • Dispatchers.IO 不会阻塞线程

所以最终结果为:

1,4,2,3,5,6,7

这个例子稍微有点复杂,正常开发中碰到这种代码建议重构!

案例7

image-20230214170510998

| |

| |

| 占位符 |

| |

| |

默认执行调度前的代码我就不过多说了,首先会执行1,7

然后通过阻塞1阻塞500ms,

此时协程1是异步的,所以会执行 2

这里有一个关键点:

一定要分清楚Job 和 coroutineContext

协程1中传入的Dispatchers.IO 是 coroutineContext, 返回的Job是异步并没有任何关系!!

协程2使用了协程1的job并不会有任何作用, 因为协程2不是异步的,所以他会等待阻塞1执行完毕后在执行

阻塞1执行完的时候,然后立即Job#cancel()了

所以协程1,协程2,协程3都会被cancel掉

最终的执行结果为:

1,7,2,9

案例8

// TODO ======== 案例8 ===================
fun main() {
    val useTime = measureTimeMillis {
        runBlocking<Unit> {
            println("main start") // 1

            val job = GlobalScope.launch { // TODO 全局协程
                println("launch 1 start") // 2
                delay(1000L) // 延迟1
                println("launch 1 end") // 3
            }

            println("main mid") // 4
            launch(context = job) { // TODO 子协程
                println("launch 2 start") // 5
                delay(2000L) // 延迟2
                println("launch 2 end") // 6
            }

            println("main end") // 7
        }
    }

    println("使用时间: $useTime")
}

| |

| |

| 占位符 |

| |

| |

这段代码有点刁钻,刚写出来的时候,我自己都没看明白是怎么回事,然后细细品味了一番才悟了!

首先执行调度前的代码是没问题的:

1,4

但此时并不会执行7, 因为在执行子协程的时候使用到了job,既然使用到了另一个job,那么就必须吧 全局协程执行完才有job

所以这里会先执行2, 然后 遇到 延迟1

所以目前的打印顺序为

1,4,2

然后子协程被创建, 并不会执行协程体中的代码,因为当前调度前的代码还没有走完

所以会先执行调度前的代码

1,4,2,7

最终你认为会执行全局作用域 子协程体中的代码?

那就大错特错了!!!

还记得runBlocking是干什么用的嘛?

runBlocking 只会等待自己子协程执行

GlobalScope#launch {} 很显然不是runBlocking 的子协程

那么,子协程肯定是runBlocking的子协程吧, 是的,没问题

可是这里的job,是使用的GlobalScope的job

job的作用是什么呢?

管理协程的生命周期等

runBlocking 都不会等待GlobalScope作用域,更不会等待使用GlobalScope job的作用域

所以这里的最终结果为:

1,4,2,7,5

完整代码

下一篇预告: 协程之间的通信

  • channel
  • produce
  • actor
  • broadcastChannel
  • select

原创不易,您的点赞就是对我最大的支持!

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

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

相关文章

电子技术——目录

电子技术——目录 第一章——信号与放大器 第二章——运算放大器 第三章——半导体器件 第四章——二极管 第五章——金属氧化物场效应晶体管 目录&#xff1a; MOS管的物理结构MOS管的CV特性MOS放大器基础MOS管的小信号模型基本MOS放大器配置MOS放大器的DC偏置分立MOS放…

传奇开服方法教程:传奇开服在哪些网站打广告?传奇发布站打广告技巧

传奇开服方法教程&#xff1a;传奇开服在哪些网站打广告&#xff1f;传奇发布站打广告技巧 纯分享经验以及方法&#xff1a;开传奇sf&#xff0c;成本最高的就是广告费用了&#xff0c;为了让服人气更高&#xff0c;主播和发布站相信你们都已经尝试了&#xff0c;上人效果如何你…

全网详解 .npmrc 配置文件:比如.npmrc的优先级、命令行,如何配置.npmrc以及npm常用命令等

文章目录1. 文章引言2. 简述.npmrc3. 配置.npmrc3.1 .npmrc配置文件的优先级3.2 .npmrc设置的命令行3.3 如何设置.npmrc4. 配置发布组件5. npm常用命令6. 重要备注6.1 yarn6.2 scope命名空间6.3 镜像出错1. 文章引言 今天在某低代码平台开发项目时&#xff0c;看到如下编译配置…

Java实现调用ChatGPT的相关接口(附详细思路)

目录1.0.简单版2.0.升级版2-1.call.timeout()怎么传入新的超时值2-2.timeout(10, TimeUnit.SECONDS)两个参数的意思&#xff0c;具体含义3.0.进阶版3-1.java.net.SocketTimeoutException: 超时如何解决4.0.终极版1.0.简单版 以下是一个使用 Java 实际请求 ChatGPT 的简单示例代…

EasyExcel详解-写Excel

一、注解使用注解很简单&#xff0c;只要在对应的实体类上面加上注解即可。ExcelProperty用于匹配excel和实体类的匹配,参数如下&#xff1a;名称默认值描述value空用于匹配excel中的头&#xff0c;必须全匹配,如果有多行头&#xff0c;会匹配最后一行头orderInteger.MAX_VALUE…

硬件系统工程师宝典(6)-----如何减小信号串扰?

各位同学大家好&#xff0c;欢迎继续做客电子工程学习圈&#xff0c;今天我们继续来讲这本书&#xff0c;硬件系统工程师宝典。 上篇我们说到描述信号传输的过程的需要做好阻抗匹配&#xff0c;以减小信号的反射。今天我们来看看传输线之间的噪声干扰&#xff0c;那就是信号的…

rdma rocev2报文格式总结

格式如下&#xff1a; wireshark抓包&#xff0c;soft-roce(基于rocev2实现的)格式如下&#xff1a; 其实这里看着有点奇怪&#xff0c;ICRC是跟在Payload头后面的&#xff0c;不知道为什么抓包看到的却是在BTH头后面的&#xff0c;还有就是看不到FCS?? 其中BTH头的格式有…

极兔一面:Dockerfile如何优化?注意:千万不要只说减少层数

说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;面试题是一个非常、非常高频的交流话题。 最近&#xff0c;有小伙伴面试极兔时&#xff0c;遇到一个面试题&#xff1a; 如果优化 Dockerfile&#xff1f; 小伙伴没有回答好&#xff0c;只是提到了减少镜像层数。…

01背包问题 AcWing(JAVA)

有 N件物品和一个容量是 V的背包。每件物品只能使用一次。 第 i件物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整数&#xff0c;N&#xff…

数据结构与算法(一):概述

数据结构学了有一年左右的时间了&#xff0c;但是一直没有详细地总结一下&#xff0c;现在回想起来&#xff0c;感觉有些内容忘记了。所以接下来一段时间我将重新归纳总结一下&#xff0c;算是温故而知新了。 一、数据结构 1、定义 数据结构是计算机存储、组织数据的方式。在…

excel学习笔记-导入外部文件,报错,数值格式变换,日期格式的转化,求和快捷键,冻结窗格

这里写目录标题一、导入外部文件1.导入csv文件2.导入txt文件3.修改txt内容&#xff0c;需要刷新才能看见更改二、报错三、数值格式变换四、日期格式的转化五、ALT &#xff0c;求和快捷键六、冻结窗格一、导入外部文件 1.导入csv文件 2.导入txt文件 3.修改txt内容&#xff0c;…

内网渗透(三十)之横向移动篇-利用远控工具向日葵横向移动

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

力扣sql简单篇练习(十六)

力扣sql简单篇练习(十六) 1 产品销售分析|| 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT p.product_id,sum(s.quantity) total_quantity FROM Product p INNER JOIN Sales s ON p.product_ids.product_id GROUP BY p.product_id1.3 运行截…

explain 每个列的含义

官网传送门&#xff1a;https://dev.mysql.com/doc/refman/5.7/en/explain-output.html 实例表 DROP TABLE IF EXISTS actor;CREATE TABLE actor (id int(11) NOT NULL,name varchar(45) DEFAULT NULL,update_time datetime DEFAULT NULL,PRIMARY KEY (id)) ENGINEInnoDB DEFA…

【Java项目】基于Java+MySQL+Tomcat+maven+Servlet的个人博客系统的完整分析

✨哈喽&#xff0c;进来的小伙伴们&#xff0c;你们好耶&#xff01;✨ &#x1f6f0;️&#x1f6f0;️系列专栏:【Java项目】 ✈️✈️本篇内容:个人博客系统前后端分离实现&#xff01; &#x1f680;&#x1f680;个人代码托管github&#xff1a;博客系统源码地址&#xff…

maven解决包冲突简单方式(插件maven helper | maven指令)

文章目录使用idea插件maven helper使用maven指令在Java开发中&#xff0c;常常会遇到不同jar包之间存在冲突的情况&#xff0c;这可能会导致编译错误、运行时异常等问题。 使用idea插件maven helper 在idea安装插件maven helper 安装重启完之后点击pom文件&#xff0c;有一个De…

元宇宙基础设施:WEB 3.0 chain33 优势分析

WEB 3.0 chain33 优势分析 一、与以太坊兼容&#xff08;优势&#xff1a;兼容及开发成本低百倍&#xff09; 使用 Solidity 开发智能合约并部署在 EVM 虚拟机上是目前以太坊生态主流的开发 模式&#xff0c;目前看来不兼容以太坊或不能与以太坊跨链都可能是不归路&#xff0c;…

AcWing语法基础课笔记 第一章 C++入门及简单的顺序结构

第一章 C入门及简单的顺序结构 编程是一种控制计算机的方式&#xff0c;和我们平时双击打开文件、关机、重启没有任何区别。 ———闫学灿 C中常用的变量类型 和所占字节大小 输出变量地址符&#xff1a; 软件环境 作业的评测与提交 在线练习地址&#xff1a;www.acwing.com …

数据库设计的基本步骤

分步设计法遵循自顶向下、逐步求精的原则&#xff0c;将数据库设计过程分解为若干相互独立又相互依存的阶段&#xff0c;每一阶段采用不同的技术与工具&#xff0c;解决不同的问题&#xff0c;从而将问题局部化&#xff0c;减少了局部问题对整体设计的影响。目前&#xff0c;此…

aws Distro for OpenTelemetry 可观测性workshop记录

参考资料 https://aws-otel.github.io/docs/introductionhttps://aws-otel.github.io/docs/introduction aws distro for opentelemetry 官方提供了不同语言不同使用场景下完善的使用实例和相关配置。 AWS Distro for OpenTelemetrics 由以下部分组成&#xff0c;用于向后端…