当前位置: 首页 > news >正文

Android-重学kotlin(协程基础)新学习总结

一、协程的基本信息

        协程,可以理解为更加轻量的线程,成千上万个协程可以同时运行在一个线程当中;协程,其实是运行在线程当中的轻量的Task;协程,不会与特定的线程绑定,它可以在不同的线程之间灵活切换。
        非阻塞注意事项:Kotlin协程的“非阻塞”其实只是语言层面的,当我们调用JVM层面的
Thread.sleep()的时候,它仍然会变成阻塞式的。与此同时,这也意味着我们在协程当中应该
尽量避免出现阻塞式的行为。尽量使用delay,而不是sleep。

二、协程的启动过程与Job生命周期

launch和射箭的共同点:
        箭一旦射出去了,目标就无法再被改变;协程一旦被launch,那么它当中执行的任务也不
会被中途改变。
        箭如果命中了猎物,猎物也不会自动送到我们手上来;launch的协程任务一旦完成了,即
使有了结果,也没办法直接返回给调用方。

launch为什么无法将结果返回给调用方呢?

源代码如下:

public
fun
CoroutineScope.launch(context:
CoroutineContext
=
EmptyCoroutineContext,start:
CoroutineStart
=
CoroutineStart.DEFAULT,block:
suspend
CoroutineScope.()
->
Unit
):
Job
{
...
}
        首先是 CoroutineScope.launch(),代表了launch其实是一个扩展函数,而它的“扩展接收者
类型”是CoroutineScope。这就意味着,我们的launch()会等价于CoroutineScope的成员方
法。而如果我们要调用launch()来启动协程,就必须要先拿到CoroutineScope的对象。前面
的案例,我们使用的GlobalScope,其实就是Kotlin官方为我们提供的一个CoroutineScope
对象,方便我们开发者直接启动协程。
        接着是第一个参数:CoroutineContext,它代表了我们协程的上下文,它的默认值是
EmptyCoroutineContext,如果我们不传这个参数,默认就会使用
EmptyCoroutineContext。一般来说,我们也可以传入Kotlin官方为我们提供的
Dispatchers,来指定协程运行的线程池。协程上下文,是协程当中非常关键的元素。
然后是第二个参数:CoroutineStart,它代表了协程的启动模式。如果我们不传这个参数,
它会默认使用CoroutineStart.DEFAULT。CoroutineStart其实是一个枚举类,一共有:
DEFAULT、LAZY、ATOMIC、UNDISPATCHED。我们最常使用的就是DEFAULT、LAZY,
它们分别代表:立即执行、懒加载执行。
推理:
        对于“suspend CoroutineScope.(Int) -> Double”这个函数类型,首先,它应该是一个“挂起函数”,同时,它还应该是CoroutineScope类的成员方法或
是扩展方法,并且,它的参数类型必须是Int,返回值类型必须是Double。
        launch()函数的第三个参数“suspend CoroutineScope.() -> Unit”,其实就能轻松分析出它的类型了。
2.使用runBlocking启动的协程会阻塞当前线程的执行
所有的代码就变成了顺序执行,但是runBlocking会阻塞当前线程的执行所以runBlocking只推荐用于连接线程与协程
3.async启动协程
async与钓鱼方式相同点:
        在我们钓鱼的时候,我们手里的鱼竿,就有点像是async当中的 Deferred对象。只要我们手
里有这根鱼竿,一旦有鱼儿上钩了,我们就可以直接拿到结果
源代码对比如下:
public
fun
CoroutineScope.launch(context:
CoroutineContext
=
EmptyCoroutineContext,start:
CoroutineStart
=
CoroutineStart.DEFAULT,block:
suspend
CoroutineScope.()
->
Unit
//
不同点1
):
Job
{}
//
不同点2
public
fun
<T>
CoroutineScope.async(context:
CoroutineContext
=
EmptyCoroutineContext,start:
CoroutineStart
=
CoroutineStart.DEFAULT,block:
suspend
CoroutineScope.()
->
T
//
不同点1
):
Deferred<T>
{}
//
不同点2
        可以发现launch和async的两个不同点,一个是 block的函数类型,前
者的返回值类型是Unit,后者则是泛型T;另外一个不同点在返回值上,前者返回值类型是
Job,后者返回值类型是Deferred。
4.Job的基础使用和结构化并发
launch、async的返回值类型分别是Job和Deferred。但是当你看Deferred的源代码,你会发现,它其实也是继承自Job的。对应的,它只是多了一个泛型参数T,还多了一个返回类型为T的await()方法。所以,不管是launch还是async,它们本质上都会返回一个Job对象
通过Job对象,我们主要可以做两件事情: 使用Job 监测协程的生命周期状态; 使用Job 操控协程。
监控协程状态:
  • isActive:判断协程是否处于活跃状态
  • isCompleted:判断协程是否已完成(正常结束或异常结束)
  • isCancelled:判断协程是否已被取消

操作协程状态:

可以通过job.cancel()方法取消协程,也可以使用withTimeout等快捷函数设置超时。

当一个协程在另一个协程的作用域内启动时,它们会建立父子关系。父协程会等待所有子协程完成后才会结束。

import kotlinx.coroutines.*fun main() = runBlocking {val job = launch {try {println("协程启动,开始执行长时间任务")repeat(1000) { i ->println("正在执行任务 $i")delay(500)}} finally {// 即使协程被取消,finally块中的代码也会执行println("协程被取消,执行清理工作")}}delay(1600) // 等待一段时间后取消协程println("准备取消协程")job.cancel()println("已调用取消方法,Job状态: active=${job.isActive}, completed=${job.isCompleted}, cancelled=${job.isCancelled}")job.join() // 等待协程彻底完成(包括finally块)println("协程最终状态: active=${job.isActive}, completed=${job.isCompleted}, cancelled=${job.isCancelled}")
}import kotlinx.coroutines.*fun main() = runBlocking {val parentJob = launch {println("父协程启动")val childJob1 = launch {println("子协程1启动")delay(2000)println("子协程1完成")}val childJob2 = launch {println("子协程2启动")delay(1000)println("子协程2完成")}println("父协程继续执行其他任务")delay(500)println("父协程即将结束")}println("主线程等待父协程完成")parentJob.join()println("父协程完成,主线程继续执行")
}

结构化并发的本质是:协程的生命周期与创建它的作用域绑定。这种绑定通过 Job 的父子关系实现,主要规则如下:

  1. 父子关系的建立

    • 在一个协程作用域内启动的子协程,会自动成为该作用域协程的子 Job。
    • 父 Job 会等待所有子 Job 完成后才能结束。
    • 父 Job 被取消时,所有子 Job 会被递归取消。
  2. 生命周期管理

    • 完成规则:父 Job 只有在所有子 Job 成功完成后才算完成。
    • 取消规则:父 Job 被取消或异常结束时,所有子 Job 会被立即取消。
  3. 异常传播

    • 默认情况下,子 Job 的异常会导致父 Job 取消,进而取消所有其他子 Job(类似 coroutineScope 的行为)。

并行任务:同时加载多个数据源

suspend fun loadUserData(): UserData = coroutineScope {// 并行启动两个异步任务val userDeferred = async { apiService.getUser() }val settingsDeferred = async { apiService.getSettings() }// 等待所有任务完成并组合结果UserData(user = userDeferred.await(),settings = settingsDeferred.await())
}

独立任务:互不影响的并行操作

lifecycleScope.launch {supervisorScope {// 这些任务独立运行,一个失败不会影响其他launch { loadImage() }launch { loadText() }launch { loadComments() }}
}

5.Job的继承(Deferred)

        在 Kotlin 协程中,Deferred<T> 是 Job 的子接口,代表一个异步计算的结果,而 await() 方法则用于获取这个结果。

await() 是 Deferred<T> 的核心方法,主要功能:

  • 挂起协程:调用 await() 会挂起当前协程,直到 Deferred 完成(成功或失败)。
  • 返回结果:如果 Deferred 成功完成,返回计算结果 T
  • 传播异常:如果 Deferred 因异常取消,await() 会抛出相同的异常。

await() 的工作原理

await() 的核心逻辑可以简化为以下步骤:

  1. 检查状态

    1. 如果 Deferred 已完成(成功或失败),直接返回结果或抛出异常。
    2. 如果 Deferred 未完成,挂起当前协程。
  2. 注册回调

    1. 将当前协程的续体(Continuation)注册到 Deferred 的完成回调中。
  3. 恢复执行

    1. 当 Deferred 完成时(无论是正常结束还是异常取消),触发回调,恢复挂起的协程。
    2. 如果成功,将结果传递给续体;如果失败,抛出异常。

6.SupervisorJob

public
fun
SupervisorJob(parent:
Job?
=
null)
:
CompletableJob
=
SupervisorJobImpl(parent)
public
interface
CompletableJob
:
Job
{public
fun
complete():
Booleanpublic
fun
completeExceptionally(exception:
Throwable):
Boolean
}
        根据以上代码,我们可以看到,SupervisorJob()其实不是构造函数,它只是一个普通的顶层 函数。而这个方法返回的对象,是Job的子类。SupervisorJob与Job最大的区别就在于,当它的子Job发生异常的时候,其他的子Job不会受到牵连。

三、协程的挂起和恢复

1.协程怎么避免回调地狱

Java的回调地狱

getUserInfo(new
CallBack()
{@Overridepublic
void
onSuccess(String
user)
{if
(user
!=
null)
{System.out.println(user);getFriendList(user,
new
CallBack()
{@Overridepublic
void
onSuccess(String
friendList)
{if
(friendList
!=
null)
{System.out.println(friendList);getFeedList(friendList,
new
CallBack()
{@Overridepublic
void
onSuccess(String
feed)
{if
(feed
!=
null)
{System.out.println(feed);}}});}}});}}
});
所以这时候,就该轮到Kotlin协程出场了。让我们用协程的挂起函数,来重构上面的代码:
//
代码段3
val
user
=
getUserInfo()
val
friendList
=
getFriendList(user)
val
feedList
=
getFeedList(friendList)
        所谓的挂起函数,其实就是比普通的函数多了一个suspend关键字而已
        以及Continuation本质上也就是一个带有泛型参数的CallBack, 只是它的名字看起来有点吓人而已。这个“从挂起函数转换成CallBack函数”的过程,被叫做是CPS转换(Continuation-Passing-Style Transformation)。
协程与挂起函数之间的关系:挂起函数,只能在协程当中被调用,或者是被其他挂起函数调用
2.什么是CPS转换

四、kotlin同步机制

Mutex(互斥锁)

        核心概念作用:确保同一时刻只有一个协程访问共享资源,防止竞态条件。

        与线程锁的区别:Mutex 的 lock() 是挂起函数,不会阻塞线程,仅挂起当前协程,待锁释放后恢复执行。

        状态:有「锁定」和「未锁定」两种状态,同一时刻只能被一个协程持有。

2. 关键方法

        lock():挂起协程,直到获取锁。unlock():释放锁,唤醒等待的协程。withLock(block):简化用法,自动释放锁(等价于 try-finally)。

public
interface
Mutex
{public
val
isLocked:
Boolean//




注意这里//







↓public
suspend
fun
lock(owner:
Any?
=
null)public
fun
unlock(owner:
Any?
=
null)
}
可以看到,Mutex是一个接口,它的lock()方法其实是一个挂起函数。而这就是实现非阻塞式
同步锁的根本原因。
//
代码段9
fun
main()
=
runBlocking
{val
mutex
=
Mutex()var
i
=
0val
jobs
=
mutableListOf<Job>()repeat(10)
{val
job
=
launch(Dispatchers.Default)
{repeat(1000)
{try
{mutex.lock()i++i/0
//
故意制造异常mutex.unlock()}
catch
(e:
Exception)
{println(e)}}}jobs.add(job)}jobs.joinAll()println("i
=
$i")
}
//
程序无法退出
以上代码会在mutex.lock()、mutex.unlock()之间发生异常,从而导致mutex.unlock()无法被调用。这个时候,整个程序的执行流程就会一直卡住,无法结束。
所以,为了避免出现这样的问题,我们应该使用Kotlin提供的一个扩展函数: mutex.withLock{} 
fun
main()
=
runBlocking
{val
mutex
=
Mutex()var
i
=
0val
jobs
=
mutableListOf<Job>()repeat(10)
{val
job
=
launch(Dispatchers.Default)
{repeat(1000)
{//
变化在这里mutex.withLock
{i++}}}jobs.add(job)}jobs.joinAll()println("i
=
$i")
}
//
withLock的定义
public
suspend
inline
fun
<T>
Mutex.withLock(owner:
Any?
=
null,
action:
()
->
T):
T
{lock(owner)try
{return
action()}
finally
{unlock(owner)}
}
可以看到,withLock{} 的本质,其实是在finally{} 当中调用了unlock()。这样一来,我们就再
也不必担心因为异常导致unlock()无法执行的问题了。

五、基础总结

        Kotlin协程是运行在线程中的轻量Task,可在多个线程间灵活切换,成千上万个协程能同时运行在一个线程中,其“非阻塞”是语言层面的,应避免使用Thread.sleep()等阻塞式行为,尽量用delay;

        启动协程的方式有launch、runBlocking和async,launch是CoroutineScope的扩展函数,返回Job,无法返回结果,其参数包括协程上下文(CoroutineContext)、启动模式(CoroutineStart,如DEFAULT代表立即执行、LAZY代表懒加载执行)和挂起函数块,runBlocking启动的协程会阻塞当前线程,仅推荐用于连接线程与协程,async返回Deferred(继承自Job,多了泛型参数和await()方法),可通过await()获取结果;

        Job用于监测和操控协程生命周期,有isActive、isCompleted、isCancelled等状态,可通过cancel()取消协程,协程间存在父子关系,构成结构化并发,父协程需等所有子协程完成才结束,父协程取消会导致子协程递归取消,子协程异常默认会导致父协程及其他子协程取消,而supervisorScope中的任务相互独立,一个失败不影响其他;

        协程通过挂起函数和CPS转换避免回调地狱,挂起函数需在协程或其他挂起函数中调用;Kotlin的同步机制使用Mutex,其lock()是挂起函数,不会阻塞线程,withLock{}可自动释放锁,避免因异常导致锁无法释放的问题。

http://www.lryc.cn/news/583406.html

相关文章:

  • MATLAB基于voronoi生成三维圆柱形
  • 2025年人工智能、虚拟现实与交互设计国际学术会议
  • Matlab-Simulink之步长
  • PlantUML 在 IDEA 中文版中的安装与使用指南
  • VR重现红军过雪山:一场穿越时空的精神洗礼​
  • VR/AR在HMI中的创新应用:远程协作与维修的沉浸式体验
  • 【图像处理基石】图像超分辨率有哪些研究进展值得关注?
  • 【SQL】使用UPDATE修改表字段的时候,遇到1054 或者1064的问题怎么办?
  • git上传大文件启用LFS git报错 the remote end hung up unexpectedly
  • ReactNative【实战系列教程】我的小红书 6 -- 购物(含商品搜索、商品分类、商品列表)
  • 【RidgeUI AI+系列】猜密码游戏
  • 2025快手创作者中心发布视频python实现
  • Python 项目快速部署到 Linux 服务器基础教程
  • Android 系统默认代码,如何屏蔽相册分享功能
  • cJSON数组操作函数使用指南
  • AJAX 学习
  • Go语言高级面试必考:切片(slice)你真的掌握了吗?
  • 11.7 ChatGPT奖励模型完全解读:RLHF核心技术深度剖析与Hugging Face实战
  • 从传统到智能:地质灾害风险评估、易发性分析与灾后重建;AI大语言模型DeepSeek、ChatGPT、GIS、Python和机器学习深度融合
  • 李宏毅NLP-9-语音转换
  • 大数据在UI前端的应用深化:用户行为模式的挖掘与预测性分析
  • Java基础--stream的使用
  • 学术绘图(各种神经网络)
  • 数据结构--堆的实现
  • 【04】MFC入门到精通——MFC 自己手动新添加对话框模板 并 创建对话框类
  • 【PDF提取内容改名】批量提取pdf多个指定区域内容到excel表格的操作步骤和方法
  • 专题:2025母婴行业洞察报告|附60+份报告PDF汇总下载
  • Context Engineering:从Prompt Engineering到上下文工程的演进
  • React、Vue、Angular的性能优化与源码解析概述
  • 深度学习 必然用到的 微积分知识