面试官问:什么是协程?协程到底是什么东西?
lipiwang 2024-11-01 14:10 7 浏览 0 评论
一,协程到底是什么东西?
官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
是不是没有看明白?我们先看一下线程的定义:
线程:
线程是指进程内的一个执行单元,也是进程内的可调度实体。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程:
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。从技术的角度来说,“协程就是你可以暂停执行的函数”。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器
kotlin中协程与线程的区别:
- 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
- 线程进程都是同步机制,而协程则是异步。
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
- 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
- 协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
总结:协程的几个关键点:
- 能够挂起和恢复
- 程序自己处理挂起和恢复
- 自己处理挂起和恢复操作实现协程的运行
一句话总结,协程其实就是函数,一段可以被挂起和恢复的函数,并且挂起恢复是由开发者自己控制操作的。执行过程是通过主动挂起出让运行权实现协作。
二,我的第一个协程
项目添加依赖:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
接下来写下人生第一个协程代码:
private fun launchCoroutine() {
GlobalScope.launch {
log("这是我的第一个协程")
}
log("launchCoroutine1")
}
I/System.out: 协程demo launchCoroutine1 main
I/System.out: 协程demo 这是我的第一个协程 DefaultDispatcher-worker-1
复制代码
引用一个使用Retrofit网络请求的例子,深入的理解下协程的执行机制
GlobalScope.launch(Dispatchers.Main) {
setPageState(ChatRoomApiService.getPageInfo("roomId").await())
}
上面的例子中,通过GlobalScope(这是一个作用域,后面介绍)launch方法启动了一个协程,介绍下这个方法:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
....
}
介绍方法的几个参数:
CoroutineContext:协程上下文
可以有很多作用,包括携带参数,拦截协程执行等等,多数情况下我们不需要自己去实现上下文,只需要使用现成的就好。上下文有一个重要的作用就是线程切换,Kotlin协程使用调度器来确定哪些线程用于协程执行,Kotlin提供了调度器给我们使用
CoroutineStart:启动模式
主要控制协程体执行时的模式,主要包含四种:
启动模式 | 作用 |
DEFAULT | 默认的模式,立即执行协程体 |
LAZY | 只有在需要的情况下运行 |
ATOMIC | 立即执行协程体,但在开始运行之前无法取消 |
UNDISPATCHED | 立即在当前线程执行协程体,直到第一个 suspend 调用 |
suspend CoroutineScope.():协程体
协程体是一个用suspend关键字修饰的一个无参,无返回值的函数类型。被suspend修饰的函数称为挂起函数,与之对应的是关键字resume(恢复),注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。
例子解析:
setPageState(ChatRoomApiService.getPageInfo("roomId").await()) 由于协程体的上下文定义的是Dispatchers.Main,所以整体方法都是运行在主线程中,http请求时会切换到子线程,retrifit返回的是Deferred对象
public interface Deferred<out T> : Job {
public suspend fun await(): T
返回响应结果后,setPageState由会切换到主线程。实现了记录切换时状态挂起,切换后恢复状态运行的结果
那么这一切是怎么得来的呢?
其实await这个方法真是的签名为:
kotlinx/coroutines/Deferred.await (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
之所以协程的调用看起来是同步的调用,完全得益于编译器的加持,对于kotlin来说,每一个 suspend 函数都是一个挂起点,意味着对于当前协程来说,每遇到一个 suspend 函数的调用,它都有可能会被挂起。每一个 suspend 函数都被编译器插入了一个 Continuation 类型的参数用来保存当前的调用点
我们用一些伪代码来描述下:
ChatRoomApiService.getPageInfo("roomId").await(object: Continuation<PageInfo>{
override fun resumeWithException(exception: Throwable){
log("exception " + exception)
}
override fun resume(value: PageInfo) {
setPageState(value)
}
})
fun await(continuation: Continuation){
handle.post{
continuation.resume(ChatRoomApiService.getPageInfo("roomId"))
}
}
三,协程的启动模式
之前已经初步介绍了协程的4中启动模式
- DEFAULT
- LAZY
- ATOMIC
- UNDISPATCHED
下面我们来具体介绍下用法:
DEFAULT 是即可启动的模式,在调用launch之后,等到调度器腾出手来了就开始执行,
所以在上面“第一个协程”的例子里,两个日志的输出顺序是不固定的。
LAZY 是等待启动模式,在调用launch之后,协程体不会立即执行,而是会返回Job实例,我们可以在实际需要的调用start()或者join()来触发协程启动
- Job.start() 立即开始执行协程
- Job.join() 需要等到协程执行完后,才可以输出
private suspend fun joinCoroutine() {
log(" joinCoroutine1 ")
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
log(" joinCoroutine2 ")
delay(300)
log(" joinCoroutine5 ")
}
log(" joinCoroutine3 ")
job.join()
log(" joinCoroutine4 ")
}
输出结果一定是:
I/System.out: 协程demo joinCoroutine1 DefaultDispatcher-worker-1
I/System.out: 协程demo joinCoroutine3 DefaultDispatcher-worker-1
I/System.out: 协程demo joinCoroutine2 DefaultDispatcher-worker-1
I/System.out: 协程demo joinCoroutine5 DefaultDispatcher-worker-2
I/System.out: 协程demo joinCoroutine4 DefaultDispatcher-worker-2
ATOMIC 立即执行协程体,但在开始运行之前无法取消。不过会被标记为cancel状态,在协程支持取消时取消
UNDISPATCHED 协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点。
举例:
private suspend fun undispatchedCoroutine() {
log(" undispatchedCoroutine1 ")
val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
log(" undispatchedCoroutine2 ")
delay(300)
log(" undispatchedCoroutine5 ")
}
log(" undispatchedCoroutine3 ")
job.join()
log(" undispatchedCoroutine4 ")
}
GlobalScope.launch(Dispatchers.Main) {
undispatchedCoroutine()
}
输出结果:
I/System.out: 协程demo undispatchedCoroutine1 main
I/System.out: 协程demo undispatchedCoroutine2 main
I/System.out: 协程demo undispatchedCoroutine3 main
I/System.out: 协程demo undispatchedCoroutine5 DefaultDispatcher-worker-2
I/System.out: 协程demo undispatchedCoroutine4 main
说明:delay() 会默认开始子线程操作。
四,协程的上下文
说到上下文,就要介绍下另外两个东东,拦截器和调度器。
抛砖引玉,先看看这三者之间的关系
国际惯例,看类先看接口:
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
......
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
......
}
上下文其实是一个集合(List)的数据结构,包含了方法get(),plus(),minusKey() 这些方法,并且CoroutineContext作为一个集合,存放的类型为Element,Element也是继承自CoroutineContext,所以就是集合套一个集合。所以大家可以理解了,“为什么上下文可以添加很多个?”。每一个Element的都有一个key,作为存放在结构体中的索引。
举一个最简单的使用例子:
private fun coroutineName(){
GlobalScope.launch(Dispatchers.Main + CoroutineName("我是测试协程")) {
log(" coroutineName " + coroutineContext[CoroutineName]?.name)
}
}
这里插入了CoroutineName,可以使用coroutineContext.get(Key)的方法来判断当前在哪个协程下。
协程拦截器
拦截器ContinuationInterceptor,也是继承的协程的上下文。大家想到拦截器,会不会想到OkHttp的拦截器,对!差不多个意思,拦截器作为上下文的一部分,塞到上下文的list里后,他的触发行为会放到最后。
下面举一个很简单的拦截器的方法,
class CustomIntercept : ContinuationInterceptor {
override val key: CoroutineContext.Key<*>
get() = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
return CustomContinuation(continuation)
}
}
class CustomContinuation<T>(val continuation: Continuation<T>) : Continuation<T> {
override fun resumeWith(result: Result<T>) {
toast("你被拦截了")
continuation.resumeWith(result)
}
override val context: CoroutineContext
get() = continuation.context
}
private suspend fun coroutineInterceptor() {
log(" coroutineInterceptor1 ")
val deferred = GlobalScope.async(CustomIntercept()) {
log(" coroutineInterceptor2 ")
delay(2000)
"123"
}
log(" coroutineInterceptor3 ")
deferred.await()
log(" coroutineInterceptor4 ")
}
结果:
I/System.out: 协程demo coroutineInterceptor1 main
I/System.out: 协程demo 你被拦截了
I/System.out: 协程demo coroutineInterceptor2 main
I/System.out: 协程demo coroutineInterceptor3 main
I/System.out: 协程demo 你被拦截了
I/System.out: 协程demo coroutineInterceptor4 main
我的个人理解:自定义拦截器其实实现拦截器的接口,还记得之前说的Continuation类吗(每个suspend里都会插入的参数),其实就是在resumeWith基础上在包一层,借此可以实现拦截器的工作。
这里说一个小的注意点:如果是自己实现了拦截器,那就会在当前的线程下启动线程,不会像默认的切换线程,需要切换的话,可以配置默认的线程池。(不细说了,使用线程池扩展方法 .asCoroutineDispatcher()可以转化为调度器)
调度器
它本身是协程上下文的子类,同时实现了拦截器的接口, dispatch 方法会在拦截器的方法 interceptContinuation 中调用,进而实现协程的调度。所以如果我们想要实现自己的调度器,继承这个类就可以了,不过通常我们都用现成的,它们定义在 Dispatchers 当中
调度器类型 | 说明 |
Dispatchers.Main | 使用这个调度器在 Android 主线程上运行一个协程。可以用来更新UI 。在UI线程中执行 |
Dispatchers.IO | 这个调度器被优化在主线程之外执行磁盘或网络 I/O。在线程池中执行 |
Dispatchers.Default | 这个调度器经过优化,可以在主线程之外执行 cpu 密集型的工作。例如对列表进行排序和解析 JSON。在线程池中执行。 |
Dispatchers.Unconfined | 在调用的线程直接执行。 |
利用调度器实现最简单的异步刷新UI的功能:
private suspend fun mockRequest() = suspendCoroutine<String>{ continuation ->
getRoomInfo() {
continuation.resume(it)
}
}
btSix.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
btSix.text = mockRequest()
}
}
五,协程作用域
说到协程的作用域,我们在写代码时,经常会开启一个协程,通常我们使用的方法是GlobeScope来单独启动一个协程的作用域,这个表明这个是一个独立的顶级的协程作用域。现在再给大家介绍两个操作符。
- coroutineScope
- supervisorScope
coroutineScope:我们叫它“一个失败,全部失败”。什么意思呢?其实就是有coroutineScope创建的作用域,如果子协程中出现了未捕获的异常,则外层的整体协程都会停止。同时使用coroutineScope会继承上下文所创建的作用域
supervisorScope:我们叫它“一个失败,随你便”。相对的好理解了,在supervisorScope创建的作用域内,其实只是父协程向子协程传递的过程,如果一个子协程出现了异常,不会影响其他子协程的任务,只会把异常抛到外层协程处理。
举个例子哦:
suspend fun coroutineTest() {
try {
log("1")
coroutineScope {
val job0 = launch {
log("2")
delay(1000)
log("3")
}
launch {
log("4")
throw NullPointerException("123")
}
try {
job0.join()
} catch (e: Exception) {
log(" 5 ${e.toString()}")
}
}
log("6")
} catch (e: Exception) {
log(" 结果 ${e.toString()}")
}
}
GlobalScope.launch(exceptionHandler) {
coroutineTest()
}
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
log("Throws an exception with message: ${throwable.message}")
}
结果:
I/System.out: 协程demo 1 DefaultDispatcher-worker-1
I/System.out: 协程demo 2 DefaultDispatcher-worker-2
I/System.out: 协程demo 4 DefaultDispatcher-worker-3
I/System.out: 协程demo 5 kotlinx.coroutines.JobCancellationException: ScopeCoroutine is cancelling; job=ScopeCoroutine{Cancelling}@956903c DefaultDispatcher-worker-1
I/System.out: 协程demo 结果 java.lang.NullPointerException: 123 DefaultDispatcher-worker-2
结果分析:协程运行时,由于coroutineScope是一个死,都死。所以1,2,4的运行没有疑问,join() 之前已经提过了,是要等到协程执行完后,才可以输出。由于抛出了NEP,所以影响了job0的协程任务,再调用join的时候,在协程里,如果join调用了已经取消的协程,就会抛出JobCancellationException,父类的协程也收到了异常的捕获NEP。
再把coroutineScope换成supervisorScope,我们再看看输出
I/System.out: 协程demo 1 DefaultDispatcher-worker-1
I/System.out: 协程demo 2 DefaultDispatcher-worker-2
I/System.out: 协程demo 4 DefaultDispatcher-worker-3
I/System.out: 协程demo Throws an exception with message: java.lang.NullPointerException: 123 DefaultDispatcher-worker-3
I/System.out: 协程demo 3 DefaultDispatcher-worker-2
I/System.out: 协程demo 6 DefaultDispatcher-worker-2
结果分析:1,2,4的运行还是一样的,当抛出NEP时,被最外层协程的异常捕获,但是并不影响job0的任务,log(3)可以正常输出。
总结下:
- 如果没有协程作用域,可以使用GlobalScope
- 可以根据实际情况决定coroutineScope和supervisorScope,互不干扰时使用supervisorScope,互相干扰则用coroutineScope
- 协程的异常可以用上下文CoroutineExceptionHandler 来捕获。
启动方式async的使用
和launch一样,async也是启动协程的工具,但是和launch最大的区别有几点
- async支持并发,async是不会阻塞线程的
- async使用后,返回参数Deferred,支持返回参数
在一个协程内,如果launch启动了两个子协程,一定是按顺序执行,但是如果换成了async,就可以异步执行,互相不受影响。使用deferred.await()可以返回参数。
举个例子:
GlobalScope.launch {
log("1")
requestA()
requestB()
log(" 结果 ")
}
private suspend fun requestA() : String{
delay(2000)
return "A"
}
private suspend fun requestB() : String{
delay(3000)
return "B"
}
结果:
2021-09-24 17:33:17.555 23743-23868/com.yupaopao.myapplication I/System.out: 协程demo 1 DefaultDispatcher-worker-1
2021-09-24 17:33:22.563 23743-23868/com.yupaopao.myapplication I/System.out: 协程demo 结果 DefaultDispatcher-worker-1
耗时了5s 同一个协程体里,是按照顺序执行的
如果我们用async和await改造一下
GlobalScope.launch {
log("1")
val deferredA = async { requestA() }
val deferredB = async { requestB() }
log(" 结果 ${deferredA.await()} ${deferredB.await()}")
}
结果:
2021-09-24 17:35:45.200 I/System.out: 协程demo 1 DefaultDispatcher-worker-1
2021-09-24 17:35:48.210 I/System.out: 协程demo 结果 A B DefaultDispatcher-worker-1
耗时了3秒,所以两个协程是并发执行的。
最后,本次只是简单的带大家了解下协程的一些概念和用法,想要做到精通,还需要在项目中实践起来,也欢迎大家可以一起学习使用,帮助提升代码质量和效率。
相关推荐
- ubuntu单机安装open-falcon极度详细操作
-
备注:以下操作均由本人实际操作并得到验证,喜欢的同学可尝试操作安装。步骤一1.1环境准备(使用系统:ubuntu18.04)1.1.1安装redisubuntu下安装(参考借鉴:https://...
- Linux搭建promtail、loki、grafana轻量日志监控系统
-
一:简介日志监控告警系统,较为主流的是ELK(Elasticsearch、Logstash和Kibana核心套件构成),虽然优点是功能丰富,允许复杂的操作。但是,这些方案往往规模复杂,资源占用高,...
- 一文搞懂,WAF阻止恶意攻击的8种方法
-
WAF(Web应用程序防火墙)是应用程序和互联网流量之间的第一道防线,它监视和过滤Internet流量以阻止不良流量和恶意请求,WAF是确保Web服务的可用性和完整性的重要安全解决方案。它...
- 14配置appvolume(ios14.6配置文件)
-
使用AppVolumes应用程序功能,您可以管理应用程序的整个生命周期,包括打包、更新和停用应用程序。您还可以自定义应用程序分配,以向最终用户提供应用程序的特定版本14.1安装appvolume...
- 目前流行的缺陷管理工具(缺陷管理方式存在的优缺点)
-
摘自:https://blog.csdn.net/jasonteststudy/article/details/7090127?utm_medium=distribute.pc_relevant.no...
- 开源数字货币交易所开发学习笔记(2)——SpringCloud
-
前言码云(Gitee)上开源数字货币交易所源码CoinExchange的整体架构用了SpringCloud,对于经验丰富的Java程序员来说,可能很简单,但是对于我这种入门级程序员,还是有学习的必要的...
- 开发JAX-RPC Web Services for WebSphere(下)
-
在开发JAX-RPCWebServicesforWebSphere(上)一文中,小编为大家介绍了如何创建一个Web服务项目、如何创建一个服务类和Web服务,以及部署项目等内容。接下来小编将为大...
- CXF学习笔记1(cxf client)
-
webservice是发布服务的简单并实用的一种技术了,个人学习了CXF这个框架,也比较简单,发布了一些笔记,希望对笔友收藏并有些作用哦1.什么是webServicewebService让一个程序可...
- 分布式RPC最全详解(图文全面总结)
-
分布式通信RPC是非常重要的分布式系统组件,大厂经常考察的Dubbo等RPC框架,下面我就全面来详解分布式通信RPC@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》...
- Oracle WebLogic远程命令执行0day漏洞(CVE-2019-2725补丁绕过)预警
-
概述近日,奇安信天眼与安服团队通过数据监控发现,野外出现OracleWebLogic远程命令执行漏洞最新利用代码,此攻击利用绕过了厂商今年4月底所发布的最新安全补丁(CVE-2019-2725)。由...
- Spring IoC Container 原理解析(spring中ioc三种实现原理)
-
IoC、DI基础概念关于IoC和DI大家都不陌生,我们直接上martinfowler的原文,里面已经有DI的例子和spring的使用示例《InversionofControlContainer...
- Arthas线上服务器问题排查(arthas部署)
-
1Arthas(阿尔萨斯)能为你做什么?这个类从哪个jar包加载的?为什么会报各种类相关的Exception?我改的代码为什么没有执行到?难道是我没commit?分支搞错了?遇到问题无法在...
- 工具篇之IDEA功能插件HTTP_CLENT(idea2021插件)
-
工具描述:Java开发人员通用的开发者工具IDEA集成了HTTPClient功能,之后可以无需单独安装使用PostMan用来模拟http请求。创建方式:1)简易模式Tools->HTTPCl...
- RPC、Web Service等几种远程监控通信方式对比
-
几种远程监控通信方式的介绍一.RPCRPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果。这个请求包括一个参数集和一个文本集,通常形成“classname.meth...
- 《github精选系列》——SpringBoot 全家桶
-
1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)