Kotlin协程进阶王炸之作-Kotlin的协程到底是什么

Kotlin协程进阶之不得不看

kotlin协程推出至今已成为 Android 开发人员的必备技能,但直到今天仍然有很多关于kotlin协程底层的争议。本篇文章围绕kotlin协程底层结合着一些基础讲解,希望可以探究明白kotlin到底是什么,当然,笔者知识有限而如果有不周错误之处希望大家指出。

很多语言中都有协程这一概念,很多人把这些不同语言的协程混为一谈,统称协程是 “轻量级线程” 。但深究其理,像 Go 那样具有独立栈的协程真正作为代码运行单元的协程才能说是轻量级线程,像kotlin这种语言层面实现的无栈协程,只能说是轻量级任务,kotlin协程是依靠状态机模拟栈实现的,完全不符合轻量级线程的定义,下面会详细说到。

协程的启动

协程本质上 = Context + Job + Dispatcher + Continuation。其中JobDispatcherContext的核心元素,这部分和协程作用域密切相关,放在作用域的内容中。Continuation(续体)是解决回调地狱的重要元素,放在解决回调地狱的内容中。这里先看一下协程的三种启动方式

  1. runBlocking:启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是协程体中的最后一行。但是只能在当前线程运行事件循环,一般仅用在调试阶段,不会用于实际开发。
  2. launch:启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。无业务返回值(业务返回值的定义是函数或协程执行完成后,向调用方返回的、与具体业务逻辑相关的结果数据),launch返回的Job是一个协程的句柄,下面协程作用域会详细说。
  3. async:Deferred<T>启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。DeferredJob的子接口,可以通过await方法得到协程体中最后一行的业务返回值。

协程作用域

协程作用域本质是一个管理协程的容器核心作用就是规定其所管理协程的生命周期,避免内存泄漏或程序崩溃。通过runBlockinglaunchasync启动的协程体等同于协程作用域,支持嵌套启动任意多的子协程,这也就是结构化并发,当父任务完成或者异常终止时,子任务也会相应地被处理。

内置作用域

安卓开发有如下内置的作用域

    1. runBlocking:阻塞式作用域(测试 / 主线程临时阻塞)

runBlocking不是普通的CoroutineScope,但它会创建一个阻塞当前线程的协程作用域,直到作用域内所有协程执行完毕。因此仅用于测试代码main函数,严禁在安卓主线程 / 生产代码中使用(会阻塞线程)。

// runBlocking 构建的作用域,阻塞当前线程funtestRunBlockingScope()=runBlocking{launch{// 继承 runBlocking 的作用域delay(1000)Log.d("scope","runBlocking 内的协程")}}
    1. GlobalScope:全局作用域

GlobalScope是 Kotlin 提供的全局静态作用域,生命周期与应用进程绑定,无法自动取消,容易导致内存泄漏。启动的协程不受任何组件生命周期管理,即使页面销毁,协程仍会运行。

funtestGlobalScope(){GlobalScope.launch{// 全局作用域的协程delay(1000)Log.d("scope","GlobalScope 协程")}}
    1. 安卓官方库提供的与组件生命周期绑定的作用域,协程会随组件销毁自动取消,是安卓开发的首选
作用域绑定的组件用途
lifecycleScopeActivity/Fragment执行与页面生命周期绑定的协程(比如请求数据、更新 UI)
viewModelScopeViewModel执行与ViewModel生命周期绑定的协程(比如数据请求,页面销毁后ViewModel仍存活时协程继续)

自定义作用域

除了系统内置的作用域外,我们还可以自定义协程作用域。我们可以理解协程作用域是为协程制定统一的运行规则(比如默认跑在哪个线程、异常怎么处理、什么时候全部取消),而 “自定义作用域” 就是我们自己来制定这些规则,想要自己制定规则,我们有必要继续深挖一下协程作用域

协程作用域CoroutineScope本身是一个接口,源码极其简洁。CoroutineScope的核心只有一个属性coroutineContext。因此,CoroutineScope包含的“内容”本质上是coroutineContext所封装的协程上下文元素。

publicinterfaceCoroutineScope{publicvalcoroutineContext:CoroutineContext}

CoroutineContext(协程上下文)是一个键值对集合,主要由如下几个元素组成。

    1. Job(作业):协程生命周期的管理者

JobCoroutineContext的核心元素之一,提供了控制协程生命周期(启动、取消、等待完成)、查询状态(是否活跃 / 完成 / 取消)的所有接口,协程构建器(launch/async)都会返回Job相关对象,Job的生命周期如下。

状态说明对应属性
New(新建)协程已创建但未启动(仅手动创建 Job 时会出现,launch会自动启动)isActive = false
Active(活跃)协程正在执行isActive = true
Completing协程执行完毕,但子 Job 还未完成(内部状态,对外表现为 Active)-
Cancelling协程被取消,正在执行收尾逻辑(如finallyisCancelled = true
Cancelled协程已取消且收尾完成isCompleted = true+isCancelled = true
Completed协程正常执行完毕(无取消、无异常)isCompleted = true+isCancelled = false

对于可取消的Job(有挂起点,如delay()withContext()),可以直接调用job.cancel()取消,终止整个协程。

每个协程对应一个Job,作用域的Job通常作为“父 Job”,其管理的所有子协程(通过作用域启动的协程)会形成父子 Job 树,父 Job 取消时所有子 Job 会被级联取消(结构化并发的核心),并且普通Job会因子协程异常导致父级失效,前者是为了避免资源泄漏程序崩溃的有利设计,后者则完全不符合业务逻辑。

因此在Job的基础上诞生了一个特殊的子类SupervisorJob,重写了异常传播逻辑:当子协程抛出未捕获的异常时,异常仅终止当前子协程,不会传播到父SupervisorJob,父级和其他子协程可以继续执行。

像上面安卓内置的**lifecycleScopeviewModelScope就用的是SupervisorJob**

    1. Dispatcher(调度器):协程运行的线程/线程池
  • Dispatcher(调度器)决定了协程运行的线程或线程池。它是CoroutineContext中最常用的元素之一,通过Dispatchers工厂类创建。常见调度器如下

    • Dispatchers.Main:主线程(适用于 Android UI 更新、iOS 主线程等);
    • Dispatchers.IO:IO 线程池(适用于网络请求、文件读写、数据库操作等阻塞 IO 任务);
    • Dispatchers.Default:CPU 密集型线程池(适用于复杂计算、排序、解析等 CPU 密集型任务);
    • Dispatchers.Unconfined:无固定调度器(协程在当前线程启动,挂起后恢复时可能切换到其他线程,慎用)。
    1. CoroutineExceptionHandler异常处理机制

CoroutineExceptionHandler用于处理作用域内未try-catch捕获的异常(仅对“根协程”有效,子协程的异常需通过其他方式处理)。当协程抛出未捕获的异常(如RuntimeException)时,异常处理器会被触发,一般就用作日志记录。注意,对于try-catch捕获了的异常,是不会触发CoroutineExceptionHandler的。

valhandler=CoroutineExceptionHandler{_,e->println("捕获异常:$e")}valscope=CoroutineScope(SupervisorJob()+Dispatchers.IO+handler)//会触发CoroutineExceptionHandlerscope.launch{throwRuntimeException("Oops!")// 未被捕获的异常}

launch启动协程遇到未捕捉的异常直接就触发CoroutineExceptionHandler了,但是对于async启动的协程,async暂存异常,直到调用.await()时才抛出。如果不await仍然不会触发CoroutineExceptionHandler

valdeferred=async{throwIOException()}//异常被暂存,不会触发 CoroutineExceptionHandler// 必须awaitdeferred.await()// 异常未被捕获,向上传播
  • 4.取消模型

普通子协程的 Job 执行完任务才会自动结束,我们使用自定义的协程作用域并不能关联页面的生命周期,当用户退出某个耗时页面不希望再进行时,如果不手动取消该协程仍然会进行下去,因此一般补充一个取消的方法

funclear(){job.cancel()}

综合以上几点,我们把以上的几个要点全部组装一起,就写出一个标准的Scope了

classUseCaseScope(dispatcher:CoroutineDispatcher):CoroutineScope{privatevaljob=SupervisorJob()overridevalcoroutineContext=job+dispatcher+CoroutineName("UseCaseScope")funclear(){job.cancel()}}

解决回调地狱

回调地狱,即多层回调嵌套的问题,例如获取用户信息 → 根据用户 ID 获取订单 → 根据订单 ID 获取商品详情,我们需要将下游函数嵌套在上游函数的回调中。对这样的代码很容易牵一发动全身,导致代码难以维护。Kotlin为解决这个问题采用了底层构建函数suspendCancellableCoroutine,用于将基于回调(Callback)或 Future 的异步 API 封装成suspend函数,从而让传统异步代码能无缝融入协程世界。

要了解回调地狱的底层逻辑,还需要学习协程的底层逻辑,深入状态机和continuation续体。简单来说,状态机就是把代码拆成“状态”,按顺序一步步走。而continuation内部1.包装了跨挂起点仍然存活的局部变量当前执行位置的对象,2.**协程上下文(CoroutineContext)**3.恢复执行的逻辑(resumeWith方法)。具体看下面例子

// 原始挂起函数suspendfunfetchData():String{valdata1=loadFromNetwork()// 挂起点1(假设 loadFromNetwork 是挂起函数)valdata2=process(data1)// 普通代码(非挂起)valdata3=loadFromDb(data2)// 挂起点2(假设 loadFromDb 是挂起函数)returndata3}//业务代码objectUserRepositoryCoroutine{// 1. 封装验证输入为挂起函数suspendfunloadFromNetwork(input:String):String{returnsuspendCancellableCoroutine{continuation->UserRepository.loadFromNetwork(input,object:ValidateCallback{overridefunonLoadFromNetworkSuccess(input:String){continuation.resume(input)// 成功了,恢复协程,返回结果}overridefunonLoadFromNetwork(msg:String){continuation.resumeWithException(IllegalArgumentException(msg))}})}}suspendfunloadFromDb(token:String):UserInfo{returnsuspendCancellableCoroutine{continuation->UserRepository.loadFromDb(token,object:UserInfoCallback{overridefunonLoadFromDbSuccess(userInfo:UserInfo){continuation.resume(userInfo)}overridefunonLoadFromDbError(msg:String){continuation.resumeWithException(SecurityException(msg))}})}}}

上面挂起函数代码和业务代码经过编译器编译后会转换为类似如下的状态机逻辑。

classFetchDataStateMachine(privatevalcompletion:Continuation<String>):Continuation<String>{varstate=0vardata1:String?=nullvardata2:String?=nulloverridefunresumeWith(result:Result<String>){valoutcome=invokeSuspend(result)// 实际编译器生成的入口方法if(outcome===COROUTINE_SUSPENDED)return// 挂起时直接返回// 非挂起情况处理结果completion.resumeWith(outcome)}privatefuninvokeSuspend(result:Result<String>):Any?{returntry{when(state){0->{state=1valr=loadFromNetwork(this)//传入当前状态机状态,检查是否挂起if(r===COROUTINE_SUSPENDED){returnCOROUTINE_SUSPENDED}// 调用挂起函数返回 COROUTINE_SUSPENDED,这里暂停执行释放线程,当前协程的 Continuation 被保存起来,直到该挂起函数调用resume(),然后继续以 Dispatcher选择的线程来执行它}1->{data1=result.getOrThrow()data2=process(data1!!)//非挂起函数,继续执行state=2loadFromDb(data2!!,this)if(r===COROUTINE_SUSPENDED){returnCOROUTINE_SUSPENDED}// loadFromDb 挂起,同上}2->{valdata3=result.getOrThrow()// 最终返回结果,不再挂起data3}else->error("Invalid state")}}catch(e:Throwable){CoroutineSingletons.RESUME_WITH_EXCEPTION}}}//业务代码的状态机伪代码,以loadFromNetwork为例internalclassLoadFromNetworkStateMachine(initialValue:String,privatevalcompletion:Continuation<String>// 这个 completion 是外层 fetchData 的 continuation):ContinuationImpl{varstate=0varinput:String?=nulllateinitvarcontinuationFromSuspendCancellable:Continuation<String>//代码里的那个 continuationoverridefuninvokeSuspend(result:Result<String>):Any?{returntry{when(state){0->{this.input=initialValue state=1// 1. 创建一个 Continuation 给 suspendCancellableCoroutine 的 lambdavalmyLocalContinuation=object:Continuation<String>{overridefunresumeWith(result:Result<String>){// 2. 当这个 myLocalContinuation.resumeWith 被调用时//其实就是你在外层回调里调用 continuation.resume(input) 触发的 这会再次进入 invokeSuspend,但 state 已经是 1 了invokeSuspend(result)}}// 3. 把 myLocalContinuation 传给 validateInput 的回调UserRepository.validateInput(this.input,object:ValidateCallback{overridefunonLoadFromNetworkSuccess(input:String){// 手动调用,它实际上调用的是 myLocalContinuation.resume(input)myLocalContinuation.resume(input)}// ...})returnCOROUTINE_SUSPENDED}1->{// 5. 当 myLocalContinuation.resume(input) 被调用后,程序会跳到这里valresult=result.getOrThrow()// 6. 将结果返回给最初调用 loadFromNetwork 的地方(也就是 fetchData 函数)completion.resume(result)returnresult}else->error("...")}}catch(e:Throwable){completion.resumeWithException(e)}}}

从这里我们可以看出,Kotlin 协程的本质是编译器的语法糖—— 编译器会把包含挂起操作的suspend函数自动编译成一个有限状态机,每个挂起点都对应一个状态。

**因为JVM 不允许也无法安全地捕获和恢复线程调用栈,所以编译器把 suspend 函数转换成状态机,用 Continuation 对象模拟栈帧。**上面的代码完全是栈帧的操作,保存执行上下文(局部变量 + 执行位置)+ 控制执行流程的功能如出一辙。

而整个回调的过程的核心就是挂起函数返回COROUTINE_SUSPENDEDif (outcome === COROUTINE_SUSPENDED) return此时if条件为真,于是resumeWith自己也return了。至此,整个协程的本次执行周期结束了。线程被释放,可以去执行其他任务(比如渲染UI、处理别的协程)。但是FetchDataStateMachine对象并没有被销毁,它作为一个Continuation对象,其内部的状态(state=1,data1=null等)都被完整地保留了下来。

当挂起的回调函数成功时调用continuation.resume(input),实际上是重新调用invokeSuspend(result)以回到原来的协程域里继续执行下面的代码。这就是协程回调将异步代码 “伪装” 成同步代码的底层逻辑。

Kotlin的协程到底是什么

在看完上面的内容基本了解了kotlin协程的底层逻辑后,我们大概知道kotlin协程的原理。开篇说了,kotlin实现的是语言层面的无栈协程,因此轻量级线程完全不适用于kotlin的协程,只能说是轻量级任务。Kotlin 协程的挂起、恢复、调度都是以函数(挂起函数suspend fun)为基本单位的,协程的执行流程被拆解为多个挂起函数的调用链,而非像线程那样以 “整个执行体” 为单位调度,它真正的并发能力来自“挂起不占线程”,而不是线程池本身。

**但是!上面一切都围绕着挂起函数来说的,如果有 10,000 个协程任务,而且里面完全没有任何挂起点(纯 CPU 计算), Kotlin 协程还能否像 Go 协程一样高效调度?答案是不能。**此时10,000 个任务会被放入线程池队列由N 个工作线程(≈ CPU 核心数)执行,一个线程一次跑一个任务,跑完才能换下一个。这正是 Kotlin 协程不是轻量级线程的铁证。

最后总结一下,Kotlin 并没有改变 JVM 的线程模型,它只是通过语言层面的突破,让函数可以中途返回并在未来恢复执行,从而实现“挂起不占线程”,用少量线程复用大量 I/O 任务。一旦协程内部没有挂起点,它就会退化为普通线程池任务, 这也是 Kotlin 协程无法被视为“轻量级线程”的根本原因。

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

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

相关文章

2026年论文降ai全攻略:5款免费降ai率工具实测!手把手教你高效降低ai率

作为一名过来人老学长,最近后台私信含“AI”量极高,大家都在问:“文章被判定疑似AI怎么办?”、“有没有好用的免费降ai率方法?”。 说实话,面对知网、维普最新的算法,以前那些“加空格、换同义词”的土办法早就…

基于时频谱图特征提取和改进型UNet卷积神经网络的机械故障诊断(Pytorch)

首先&#xff0c;将原始一维振动信号通过短时傅里叶变换转换为时频谱图&#xff0c;形成二维图像特征&#xff1b;接着构建了一个改进的UNet神经网络架构&#xff0c;该网络在保留UNet编码器-解码器结构的基础上移除了时间嵌入模块&#xff0c;增加了注意力机制和残差连接&…

基于贝叶斯物理信息神经网络的工业装备退化趋势预测方法(Pytorch)

首先对原始振动信号进行多尺度分析处理&#xff0c;同步提取时域的均方根值、峰值幅度、峭度统计特征&#xff0c;时频域的小波能量分布特征&#xff0c;以及频域的频谱能量分区特征&#xff0c;构建能够全面表征轴承健康状态的高维特征向量。 接着构建具有不确定性量化能力的…

基于图拉普拉斯正则化物理信息神经网络的工业装备退化趋势预测方法(Pytorch)

首先对采集到的原始轴承振动信号进行滑动窗口分割&#xff0c;形成等长的数据片段&#xff0c;然后对这些片段进行多尺度特征提取&#xff0c;包括计算反映信号整体能量的时域均方根值、表征冲击成分的峰值幅度、描述分布形状的峭度统计特征&#xff0c;通过小波变换提取时频域…

基于可学习Morlet小波匹配滤波和统计特征融合的引力波信号检测算法(算法完善中,Python)

算法用于引力波信号检测任务&#xff0c;结合了传统匹配滤波的物理可解释性和深度学习的学习能力。首先&#xff0c;算法定义了可学习的Morlet小波基函数&#xff0c;这些小波的频率、尺度等参数在训练过程中可以优化调整&#xff0c;能够自适应地提取引力波信号的特征。然后&a…

基于点堆动力学-热传递耦合物理模型与支持向量机残差分析的核反应堆数字孪生混合异常检测算法(以模拟信号为例,Python)

算法构建了一个核反应堆的数字孪生监控方法&#xff0c;通过点堆动力学方程与热传递方程耦合的物理模型精确模拟反应堆功率、先驱核浓度和温度的三维状态演化&#xff1b;在正常工况下采集包含测量噪声的模拟数据&#xff0c;训练支持向量机单类分类器学习物理模型预测与传感器…

基于多阶段参数辨识与蒙特卡洛不确定性传播的质子交换膜水电解槽电压退化预测和预后地平线评估集成算法(Python)

代码实现了一个完整的质子交换膜水电解槽&#xff08;PEMWE&#xff09;剩余使用寿命&#xff08;RUL&#xff09;预测与性能评估系统。整个流程从加载合成的PEMWE数据集开始&#xff0c;首先基于底层的物理退化模型计算真实的理论失效时间&#xff08;EOL&#xff09;。系统通…

基于希尔伯特变换与带通滤波的滚动轴承振动信号包络谱故障诊断算法(Python,jupyter nootbook文件)

代码实现了一套完整的轴承故障诊断流程&#xff0c;通过对采集到的振动加速度信号进行多阶段分析来检测和识别滚动轴承的不同故障类型。首先&#xff0c;代码从MAT格式的数据文件中加载不同工况&#xff08;健康、内圈故障、外圈故障、滚动体故障&#xff09;和不同转速&#x…

最小生成树专题

最小生成树专题 要补一下 kruskal重构树 boruvka 严格/非严格次小生成树 无向图中选择若干条边构成一颗树,使得无向图联通,现在要求一颗边权和最小的树,叫做最小生成树 kruskal \(O(mlogm)\) 并查集思想 贪心 边权从…

1月24号

今天和昨天并没有什么太大的区别。 依旧是java中关于web端之类的东西中基础学习。 然后看了看那些教学中的页面设计进行参考。

别再二选一了:高手都在用的微调+RAG混合策略,今天一次讲透

别再二选一了:高手都在用的微调+RAG混合策略,今天一次讲透"我们公司有大堆内部文档,想用大模型来做智能问答。有人说该用RAG,有人说该微调,我到底该听谁的?""我们公司有大堆内部文档,想用大模型…

导师严选9个一键生成论文工具,研究生论文写作必备!

导师严选9个一键生成论文工具&#xff0c;研究生论文写作必备&#xff01; AI 工具助力论文写作&#xff0c;效率与质量并重 在当前研究生论文写作过程中&#xff0c;AI 工具的应用已经逐渐成为一种趋势。随着 AIGC 技术的不断进步&#xff0c;许多学生开始借助 AI 工具来提升…

samp-cef 解决客户端显示服务端传回数据乱码问题

核心思路: 在服务端传递之前把中文数据转换为十六进制,再把这个十六进制传给客户端,客户端收到以后再把十六进制转换为gbk 服务端代码示例:// StringToHexstock StringToHex(const string[], dest[], size = sizeof…

高中学习机深度测评:告别智商税!热门机型实测对比

随着教育数字化加速,学习机已成为高中生补弱提分、高效备考的核心辅助工具。但市场上产品鱼龙混杂,从几百元到上万元不等,有的堆砌功能噱头,有的资源适配性不足,不少家长和学生陷入“越贵越好”“功能越多越实用”…

【开题答辩全过程】以 某县农村留守儿童爱心帮扶平台为例,包含答辩的问题和答案

个人简介一名14年经验的资深毕设内行人&#xff0c;语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。感谢大家的…

Day28-20260124

本文展示了Java中稀疏数组的实现过程。首先创建一个11x11的二维数组,其中只有两个非零值(1和2)。然后将其转换为稀疏数组形式:第一行存储原始数组的行列数和非零值个数,后续行存储每个非零值的行列坐标和数值。最后…

America has been dead!

Schei auf die Vereinigten Staaten von Amerika.

冲刺Day5

Scrum 冲刺博客 Day5 1. 今日站立式会议时间/地点:线上同步参会人:全员 会议纪要:对报修流程与角色权限进行补齐,补充状态流转与指派规则,输出可复现的测试脚本。 照片:无2. 昨天已完成的工作成员:全员 完成事项…

JavaScript 中 ||(逻辑或)和 (逻辑与)

&& 是「且」逻辑,遇假就停,返回第一个假值 / 最后一个真值,常用于条件执行; || 是「或」逻辑,遇真就停,返回第一个真值 / 最后一个假值,常用于设置默认值; JS 逻辑运算符返回的是「操作数本身」,而非…

数据结构——三十九、顺序查找(王道408) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …