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

kotlin中关于协程的使用

一、什么是协程?

        协程是Kotlin提供的一种轻量级的线程管理框架,它允许我们以同步的方式编写异步代码,让代码更加简洁易读。与线程相比,协程的创建和切换开销更小,一个应用程序可以轻松创建数千个协程而不会导致性能问题。

二、为什么在Android中使用协程?

  1. ​避免回调地狱​​:以顺序的方式编写异步代码
  2. ​主线程安全​​:轻松切换线程,确保UI操作在主线程执行
  3. ​简化错误处理​​:使用try-catch处理异步操作中的异常
  4. ​生命周期感知​​:与Android组件生命周期自动绑定

三、协程的使用

1、常用Api

先列举下关于协程的常用的六种的Api以及对用的各种功能,如下表:

协程的 6 种常用 API
函数作用启动协程
launch { } 串行启动无返回的协程、异常会​​立即抛出​​给父级,导致整个作用域取消。
async { }  并行

启动有返回的协程(并行)异常只在调用 .await()时​​抛出​​。

withContext(Dispatchers.x) { }一次性切换线程并返回结果
runBlocking { }阻塞当前线程直到协程完成(不常用)
coroutineScope { }子作用域,失败时全部取消
supervisorScope { }子作用域,失败时不影响兄弟协程

这6种api各自有各自的功能:

  1. ​​launch、async用于启动新协程的构建器​​ (真正意义上的“开启协程”)
  2. ​​withContext、coroutineScope、supervisorScope用于控制线程和作用的域构建器​​ (在已有协程内划分作用域)
  3. runBlocking​​一个特殊的阻塞式构建器​​ (主要用于测试,非Android日常开发)

2、使用示例

(1)、launch(无返回)
// 在 Activity / ViewModel 中
lifecycleScope.launch {Log.d("TAG", "launch 开始")delay(1000)Log.d("TAG", "launch 结束")   // 1 秒后打印
}

(2)、async(有返回,并行)
suspend fun main() = coroutineScope {val a = async { delay(800); 1 }val b = async { delay(600); 2 }val sum = a.await() + b.await()   // 两个 delay 并行跑println(sum)                      // 输出 3
}
suspend fun main() = coroutineScope {val a = async { delay(800); 1 }val b = async { delay(600); 2 }// 一行等全部,返回 List<Int>val list = awaitAll(a, b)   // 并行等待,顺序与入参一致println(list.sum())         // 输出 3
}

async开启协程的方式写了两个示例,因为这里 awaitAll() 和 await() 的使用上有点区别

方式代码异常传播适用场景
逐个 await()a.await()+b.await()第一个异常会阻断第二个数量少
awaitAll()awaitAll(a, b)合并异常,一次性抛出数量多,更整洁
        3.1在开启协程时可以指定线程的作用域

        launch和 async函数本身可以接受一个 CoroutineContext类型的参数(通过参数指定上下文),你可以通过这个参数来​​指定协程的调度器、异常处理器等​​。从广义上讲,这也是在“设置”协程运行的上下文环境。

// 在 ViewModel 的 viewModelScope 这个“父作用域”中启动新协程
viewModelScope.launch { // 这个 launch 是 viewModelScope 的子协程// 代码
}viewModelScope.async { // 这个 async 也是 viewModelScope 的子协程// 代码
}//指定线程// 设置调度器:在IO线程池运行
viewModelScope.launch(Dispatchers.IO) {// 网络请求等IO操作
}// 设置异常处理器
val exceptionHandler = CoroutineExceptionHandler { _, exception ->println("Caught $exception")
}
viewModelScope.launch(exceptionHandler) {// 可能会抛出异常的代码
}// 可以组合多个上下文元素
viewModelScope.launch(Dispatchers.Default + exceptionHandler) {// ...
}

(3)、withcontext(一次性切线程并拿结果)

withContext 可以切到 任何 Dispatcher,常用只有这3个:

Dispatcher类型线程使用场景
Dispatchers.Main主线程(UI)更新界面
Dispatchers.IO子线程池网络 / 文件 / 数据库
Dispatchers.Default子线程池CPU 密集计算

Dispatchers.IO和 Dispatchers.Default管理的都是子线程(后台线程),但它们是为​​完全不同类型的工作任务​而设计的,因此它们的底层线程池策略有显著区别。

suspend fun main() {val threadName = withContext(Dispatchers.IO) {Thread.currentThread().name     // 在 IO 线程池里执行}println(threadName)                 // 例如:DefaultDispatcher-worker-1
}
(4)、runBlocking(阻塞主线程,仅测试用)
fun main() = runBlocking {println("start")delay(1000)println("end")   // 整个 main 会等 1 秒
}

(5)、coroutineScope(子作用域,任一失败全部取消)
suspend fun main() = coroutineScope {launch {delay(200)throw RuntimeException("boom")   // 异常}launch {delay(1000)println("never reach")           // 被取消}
}

(6)、supervisorScope(子作用域,失败互不影响)
suspend fun main() = supervisorScope {launch {delay(200)throw RuntimeException("boom")   // 兄弟不受影响}launch {delay(500)println("still alive")           // 会打印}
}

  • launch / async / withContext:业务代码用的最多
  • runBlocking:单元测试时使用
  • coroutineScope / supervisorScope:并发任务时控制异常

四、挂起函数

1、什么是挂起函数

说到协程,就不得不说提到挂起函数,什么是挂起函数?

  • 标记suspend 关键字。

  • 能力:只能在协程或另一个挂起函数里调用。

  • 本质:是一种可以被协程挂起(暂停执行),而不会阻塞其所在线程的函数。编译器把函数切成「状态机」,遇到 delay()、withContext() 等挂起点就挂起,线程空出来干别的,等结果回来再恢复继续执行。

2、挂起函数是如何工作的?

挂起函数的背后是 ​​状态机​​ 和 ​​Continuation​​ 概念。

​编译器魔法​​:当你编译一个 suspend函数时,编译器会做额外的转换。它会为协程体生成一个状态机(State Machine)。每个挂起点(即调用另一个 suspend函数的地方)都成为状态机的一个可能状态。

​Continuation​​:可以把它理解为一个​​回调对象​​,它封装了 “协程在挂起之后该如何恢复执行” 的信息,包括它应该从哪一行代码继续、当时的局部变量是什么等等。

当你调用 withContext(Dispatchers.IO) { ... }时,实际上发生了:

  1. 协程在执行到 withContext时,会​​挂起​​。
  2. 它将 withContext块内的代码和 Continuation(恢复信息)一起提交给协程调度器。
  3. 调度器安排一个线程(IO线程池中的线程)来执行这个块。
  4. 执行完毕后,调度器再通知协程:“你交代的任务完成了”,并把结果和 Continuation一起,安排回原来的线程(或者你指定的调度器,如 Dispatchers.Main)。
  5. 协程根据 Continuation的信息,​​恢复​​到挂起点之后的状态,继续执行。

上面这种描述太抽象,我们不如想象成一个快递员(​​一个线程​​)在送包裹(​​执行任务​​)。

​1、普通函数(阻塞)​​:快递员到了一个办公室楼下,需要等收件人下来签字。在收件人下来之前,他什么都不做,就干等着(​​阻塞​​)。这期间他没法去送别的包裹,效率很低。

2、​回调函数(非阻塞但复杂)​​:快递员把包裹交给前台,并留下一个纸条(​​回调函数​​)说:“等收件人来了,打电话叫我回来签字”。然后他就去送别的包裹了。等前台打电话来,他再回来处理。这样效率高了,但流程变得复杂,如果包裹多,需要留很多纸条,管理起来很混乱(​​回调地狱​​)。

​3、挂起函数(挂起-恢复)​​:快递员到了办公室楼下,他给收件人打了个电话说:“我到了,你下来吧”。​​在收件人下楼的这段时间里,他并没有干等着,而是被派去送隔壁楼的另一个小包裹(挂起当前任务,线程去干别的事了)​​。等收件人快到楼下了,快递员也送完隔壁的小包裹回来了,然后顺利签字完成主要任务。

在这个比喻中:

  1. ​快递员​​:就是一个线程。
  2. ​送主要包裹​​:就是执行协程体里的代码。
  3. ​打电话让收件人下楼​​:就是调用一个 suspend函数(比如 delay, withContext, 或者你的自定义挂起函数)。
  4. ​去送隔壁的小包裹​​:线程被释放,可以去执行其他任务(可能是其他协程的任务)。
  5. ​收件人下楼完成,快递员回来签字​​:挂起的条件满足,协程在(可能是原来的,也可能是另一个)线程上​​恢复​​,继续执行后面的代码。

关键点:​​ 

        1、挂起函数不会阻塞线程,而是释放线程去干别的活,等它等待的操作(如网络请求、磁盘IO、延迟)完成后,协程会在合适的时机和线程上​​恢复​​执行。

        2、挂起函数本身不指定线程​​:suspend关键字只是一个标记,告诉编译器这个函数可以在协程中使用并可能挂起。它本身并不包含任何线程信息。线程由调度器(Dispatcher)决定​​:真正决定代码在哪个线程上运行的是协程的上下文中的 CoroutineDispatcher(协程调度器)➡️(Dispatchers.Main / IO / Default)。

        3、挂起函数的作用域不一定在子线程中。它的执行线程完全取决于它在被调用时所在的协程上下文(CoroutineContext),以及它内部使用的调度器(Dispatcher)。​挂起函数的核心是“挂起”(suspend),而不是“切换线程”。线程切换只是实现挂起的一种常用手段。

3、挂起函数的使用场景

(1)情况一:在主线程启动,并在主线程调用挂起函数

viewModelScope.launch(Dispatchers.Main) { // 1. 在主线程启动协程// 2. 当前上下文是 Dispatchers.MaindoSomeWork() // 3. 调用挂起函数
}// 这个挂起函数没有使用 withContext 切换线程
// 因此它将继承调用者的上下文,即在主线程运行
suspend fun doSomeWork() {// 这里的代码会在 Dispatchers.Main 上执行// 如果在这里执行耗时操作,会阻塞主线程!heavyOperation() // ❌ 危险!会阻塞UI!
}fun heavyOperation() {Thread.sleep(2000) // 模拟耗时阻塞操作
}

这是最常见的Android场景。协程在主线程启动,挂起函数内部没有切换上下文,那么它就会在主线程运行。在这个例子中,挂起函数 doSomeWork()的作用域是主线程。

(2)情况二:正确的“主线程安全”挂起函数

viewModelScope.launch(Dispatchers.Main) { // 1. 在主线程启动协程// 2. 当前上下文是 Dispatchers.Mainval result = doSomeSafeWork() // 3. 调用挂起函数(挂起点)updateUI(result) // 6. 恢复后,仍在主线程,安全更新UI
}// 这是一个主线程安全的挂起函数
suspend fun doSomeSafeWork(): String {// 4. 函数开始执行时,仍在主线程// 但 withContext 会将协程的执行挂起,并将代码块交给 IO 调度器return withContext(Dispatchers.IO) {// 5. 这个代码块现在在IO线程池中的某个线程执行// 模拟网络请求或数据库操作,不会阻塞主线程Thread.sleep(2000)"Result from network"}// withContext 完成后,协程会自动切回原来的上下文(Dispatchers.Main)// 所以返回值是在主线程被接收的
}

 一个良好的挂起函数应该内部处理线程切换,保证无论从哪个线程调用它,其耗时操作都在后台进行,并最终将结果返回给调用方线程。

doSomeSafeWork() 函数​​内部​​的 withContext 代码块的作用域是子线程(IO线程)。但从外部看,这个函数​​被调用和返回​​的上下文(viewModelScope通常是 Dispatchers.Main)是主线程。

(3)情况三:在子线程启动协程

viewModelScope.launch(Dispatchers.Default) { // 1. 在Default线程池启动// 2. 当前上下文是 Dispatchers.DefaultdoSomeWork() // 3. 这个挂起函数将在 Default 线程执行
}suspend fun doSomeWork() {// 在 Dispatchers.Default 上执行
}

如果明确指定一个后台调度器启动协程,那么挂起函数(如果不内部切换)就会在那个后台线程运行。

(4)总结

场景

挂起函数所在线程

说明

​默认情况​

​继承调用方协程的上下文​

挂起函数不自动切换线程,它在哪个线程被调用,就在哪个线程运行。

​使用 withContext

​由 withContext的参数决定​

这是​​主动控制​​挂起函数内部代码执行线程的标准方式。

​设计目标​

​实现主线程安全​

一个好的挂起函数应该内部使用 withContext确保任何耗时操作都不在主线程进行,让调用者无需关心线程细节。

挂起函数的作用域不一定在子线程中。它的线程环境是​​动态的​​和​​可预测的​​。

​​动态的​​:取决于调用它的协程上下文和它内部使用的调度器。

可预测的​​:开发者可以通过 withContext精确地控制其内部代码应该在哪个线程上执行。

​注意:​​ 

        1、永远不要假设一个挂起函数会在后台线程运行。如果要执行耗时操作,​​必须​​在挂起函数内部使用 withContext(Dispatchers.IO)或 withContext(Dispatchers.Default)来明确切换到合适的线程。这才是编写“主线程安全”挂起函数的关键。

        2、结构化并发(Structured Concurrency),这是协程设计的核心哲学,要求协程的生命周期与它的启动作用域(如 ViewModel的 viewModelScope或 Activity的 lifecycleScope)绑定。

       

       

http://www.dtcms.com/a/355562.html

相关文章:

  • 陕西风味马卡龙:家常自制,特色甜趣共享
  • 传输层协议介绍
  • 结构化提示词革命:JSON Prompting如何让AI输出精准如激光
  • 数字化生产管理系统 (MES)
  • 服务器核心组件:CPU 与 GPU 的核心区别、应用场景、协同工作
  • 揭开.NET Core 中 ToList () 与 ToArray () 的面纱:从原理到抉择
  • ansible常用命令的简单练习
  • Linux系统 -- 多线程的控制(互斥与同步)
  • 数学思维好题(冯哈伯公式)-平方根下取整求和
  • 个人博客运行3个月记录
  • 了解ADS中信号和电源完整性的S参数因果关系
  • Typora 教程:从零开始掌握 Markdown 高效写作
  • MySQL事务ACID特性
  • JavaScript中的BOM,DOM和事件
  • 英语单词:ad-hoc
  • BugKu Web渗透之成绩查询
  • 白杨SEO:网站优化怎么做?应用举例和适合哪些企业做?参考
  • 速成Javascript(二)
  • 新书速览|SQL Server运维之道
  • 【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
  • 激活函数篇(3):Softmax
  • maven scope 详解
  • 通信原理实验之线性均衡器-迫零算法
  • dht11传感器总结
  • [灵动微电子 MM32BIN560CN MM32SPIN0280]读懂电机MCU之串口DMA
  • 【C++游记】子承父业——乃继承也
  • 91美剧网官网入口 - 最新美剧资源在线观看网站
  • 保姆级教程 | 在Ubuntu上部署Claude Code Plan Mode全过程
  • 【论文阅读】MotionXpert:基于肌电信号的优化下肢运动检测分类
  • Spring事务管理机制深度解析:从JDBC基础到Spring高级实现