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

kotlin协程Coroutine

协程的用处

协程是协同程序,kotlin协程是一个基于Java Thread API封装的工具包。实现把异步逻辑采用顺序写法同步执行,顺序写法同步执行可以清晰简单的写复杂的逻辑,避免回调地狱简化并发管理。

异步和同步这两个概念探讨的是程序的控制流程,异步的同时也经常伴随着并发,但这不是必然的。Kotlin协程是用来简化异步程序设计的。

绝大多数协程都是语言层面自己实现,不同的编程语言有不同的使用场景,自然在实现上也看似有很大的差异。有的语言甚至没有实现协程,但开发者可以通过第三方框架提供协程的能力,例如Java的 协程框架Quasar,因此虽然协程的实现上呈现出多种多样的局面。

挂起和恢复

挂起suspend和恢复resume可以控制执行调用的转移,是实现协程功能的基本思想。挂起函数的调用处称为挂起点,挂起后任务流程暂停,线程非阻塞释放,不会产生ANR。真正的挂起需要在多线程中执行异步代码块,这也才是挂起需要的地方。挂起解决的不是使流程快速执行,而是使流程能够方便的等待异步返回,无论等待多久因为线程非阻塞释放不会ANR。

Kotlin协程的只用了一个关键字suspend来表示挂起点,包含了异步调用和回调两层含义。所有异步回调对于当前调用流程来讲都是一个挂起点,在这个挂起点可以做的事情非常多,既可以像async/await那样异步回调,又可以添加调度器来处理线程切换,还可以作为协程取消响应的位置,等等。

挂起函数

suspend:暂停、推迟、悬挂,yield:让出。

挂起指的是任务的挂起,线程对此任务暂停行,不阻塞线程被调度去执行其它任务。

加了suspend声明的方法,表明它在协程内,可以使用coroutineContext成员变量。

suspend表示函数对运行它的线程来说是非阻塞的,但在挂起函数内部所有函数调用都是阻塞的。

挂起函数必须在协程体或挂起函数中使用,如果在其它地方使用挂起函数会编译错误,因为挂起函数被调用时需要增加传递进入一个Continuation实例,在恢复时调用。实际上使用suspendCoroutine函数执行异步代码块,并向异步代码块传递Continuation实例。

挂起函数可以是顶层函数,成员函数,抽象函数。

挂起函数调用时流程挂起,执行返回后流程恢复。

Kotlin的协程可以在挂起函数范围内的任意调用层次挂起,可以在协程内其中任意嵌套suspend函数,会在最内层次的挂起函数调用时才真正的挂起。

协程与线程

内核线程,语言协程。协程是语言运行时的,在操作系统中没有协程的概念。

协程可以看作是线程的线程,它不会阻塞线程。如果线程结束了,其中的协程也就结束了。

就像线程之间是异步的、线程内是同步的,协程之间是异步的、协程内是同步的。

kotlin标准库中包含了协程编辑核心底层API,但是使用起来麻烦,协程扩展库kotlinx.coroutines中提供了高级API。

launch函数创建并启动一个协程,类似于线程的thread.start。协程中的delay相当于线程中的sleep。

当函数栈被销毁后,其中启动的协程将看不到执行打印结果。

协程体中执行的函数应该都是挂起函数??

协程的创建和调用是不阻塞的,函数的调用是阻塞的。

协程框架

kotlinx.coroutines 官方协程框架,基于标准库实现的特性封装。kotlinx包中一般提供一些框架。

协程框架的引入

//kotlin标准库,包含协程基础库 implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" //协程框架库 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" //协程Android库,提供AndroidUI调度器 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" //协程Swing库,提供SwingUI调度器 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-swing:$coroutine_version" //提供将 CompletableFuture 转换为 Kotlin 协程的机制 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutine_version"

其他特性

  • Channel:"热"数据流,并发安全的通信机制
  • Flow:"冷"数据流,协程的响应式API
  • Select:可对多个挂起事件进行等待

协程创建函数

runBlocking函数

runBlocking函数创建并启动一个阻塞式协程(仅用于桥接),runBlocking会阻塞当前线程,禁止在生产代码中使用(如 Android 主线程)。runBlocking函数来实现类似主协程的功能,runBlocking函数不是用来当做普通协程函数使用的,它的设计主要是用来桥接普通阻塞代码和挂起风格的(suspending style)的非阻塞代码的,例如用在 main 函数中,或者用于测试用例代码中。还可以使用withTimeoutwithTimeoutOrNull来限制执行时间并优雅地取消协程。

协程的join方法和线程的join方法性质类似。

在协程体中,使用withTimeout来创建并启动一个带超时的协程,超时后会抛出异常,并且它不返回Job对象。

  • ‌永远不要‌直接在 Dispatchers.IO 或其他后台调度器中更新 UI,这会导致 CalledFromWrongThreadException 异常。
  • ‌优先使用 lifecycleScope 或 viewModelScope‌,它们能自动管理协程生命周期,防止内存泄漏。
  • ‌使用 withContext(Dispatchers.Main)‌ 是在协程内切换回主线程更新 UI 的标准做法。
  • ‌避免使用 GlobalScope‌,因为它不绑定任何生命周期,容易导致内存泄漏。
  • ‌runBlocking‌ 在 Android 中应谨慎使用,因为它会阻塞当前线程,可能导致 UI 卡顿。

launch函数

launch函数以非阻塞(non-blocking)当前线程的方式,创建并启动一个新的协程后台任务,并返回一个Job对象作为当前协程的引用。

async / wait函数

async创建一个协程,但不立即执行。async返回一个延迟任务对象Deferred,一种轻量级的非阻塞性future,它是一个票据,之后在哪里调用获取结果由调用者决定。

async和wait成对使用,使用Deferred调用 await() 函数来获得其最终结果。延迟任务Deferred也是Job类型, 它继承自Job,它也有isActive、isCompleted属性,join()、cancel()函数,因此也可以取消它。

async并不表示它一定运行在非UI线程上,它运行在哪个线程上,要看调用它的是哪个作用域,或者给它指定的是哪个线程。

fun main() = runBlocking { val deferred1 = async { calculateSum(1, 100) } val deferred2 = async { calculateSum(101, 200) } val result1 = deferred1.await() val result2 = deferred2.await() println("总和:${result1 + result2}") } suspend fun calculateSum(start: Int, end: Int): Int { delay(500) var sum = 0 for (i in start..end) sum += i return sum }

调度器切换函数withContext

协程框架提供了 withContext挂起函数,在不启动新协程的情况下切换协程上下文,将执行耗时操作切换到 IO 线程或Default线程,同时主任务挂起原线程非阻塞释放,耗时操作完成后原线程恢复。withContext会将参数中的Lambda表达式调度到对应的调度器上,返回值为Lambda表达式的值。它的作用几乎等价于async{...}.await(),相比之下withContext的开销更低,因此对于使用async之后立即调用await的情况,应当优先使用withContext

写下withContext(Dispatchers.IO) { ... }时,从API层面看,是告诉协程:“这段代码可能会阻塞,请在一个适合IO操作的线程上执行它。”

fun main() = runBlocking { val data = fetchUserData() println(data) } suspend fun fetchUserData(): String = withContext(Dispatchers.IO) { delay(1000L) "User data" }

launch与withContext

用 launch(Dispatchers.IO) 的情况

  • 不需要立即获取结果
  • 需要独立控制生命周期
  • 多个任务并行无依赖

用 withContext(Dispatchers.IO) 的情况

  • 需要同步获取结果
  • UI线程协程中执行IO操作(Android最常用场景):执行完自动切回主线程。
  • 不需要独立取消

常见坑(避坑指南)

  • 坑1:在UI线程用launch(IO),执行完想更新UI → 报错!launch(IO)不会自动切回主线程,需手动用withContext(Main)切换。
  • 坑2:用withContext(IO)却不接收返回值 → 不如直接用launch(IO)。
  • 坑3:多个withContext(IO)串行执行 → 效率低!如果无依赖,改用launch(IO)并行执行,再用joinAll()等待所有任务完成。最后总结(划重点,记牢这3句)

总结

  1. 异步无返回、需独立控制 → 用 launch(Dispatchers.IO)
  2. 同步需返回、要自动切回原线程 → 用 withContext(Dispatchers.IO)
  3. Android开发 中,请求接口+更新UI → 优先用 withContext(IO)(最简洁、最安全)

可中断的调度器切换函数 runInterruptible

当需要在一个协程里调用一个会阻塞线程、且没有提供 suspend 版本的 API 时,就应该使用 runInterruptible来替换withContext。runInterruptible 不仅切换线程,更赋予了在协程取消时“中断”该线程的能力。

Kotlin 协程实践:切勿滥用 withContext(Dispatchers.IO)

https://zhuanlan.zhihu.com/p/1997738883081863430

runInterruptible用来解决当withContext所在的协程被取消时,withContext中的阻塞函数可能出现持续占用Dispatchers.IO线程池中的一个线程的问题。因为有些线程阻塞函数不会响应协作式取消(Cooperative Cancellation),但是它会响应线程中断。

当使用runInterruptible包装的协程被取消时,它会执行一个关键动作:调用运行该代码块的线程的 Thread.interrupt() 方法。收到中断事件后线程就能从阻塞中被唤醒,并从runInterruptible代码块中退出,最终被释放回Dispatchers.IO线程池,供其他任务复用。

协程上下文(coroutine contexts)

协程上下文包括一个协程调度程序, 它可以指定由哪个线程来执行协程。调度器可以将协程的执行调度到一个线程池,限制在特定的线程中;也可以不作任何限制,让它无约束地运行。使用newSingleThreadContext函数新建的协程上下文。

父子协程的层次关系

当使用协程A的上下文启动另一个协程B时, B将成为A的子协程。当父协程A任务被取消时, B以及它的所有子协程都会被递归地取消。子协程异常时,默认会向上传播取消父协程;使用supervisorScope可以改变异常传播行为。

通过GlobalScope创建的协程可以称作根协程,它没有父协程。根协程的协程体的Receiver就是作用域实例,因此可以在它的协程体内部再创建新的协程,最终产生一个协程树。

suspendCoroutine

标准库API suspendCoroutine函数用来等待异步代码的执行结果。用一个函数表达式来包装一个异步代码块,在代码块异步执行回调结束的地方,调用函数表达式的参数res:Coroutine<Any?>的resume方法返回结果。

要从 Web API 获取数据,一个简单的办法是使用 java.net.URL 实例。Kotlin 里有一个针对
URL 的扩展函数叫 readText。这个扩展函数能满足所有的需求——连接 Web API 端点,缓冲数据,以及将接收到的数据转换为字符串。需要注意,Android 明令禁止应用在主线程上访问网络,因为这会阻塞主线程,导致 UI 长时间失去响应。

协程调度器 Dispatchers

官方框架中预置了4个调度器,通过Dispatchers对象访问它们,调度器决定协程运行的线程。

  • ·Default:默认调度器,适合处理后台计算,是一个CPU密集型任务调度器。
  • ·IO:IO调度器,适合执行IO相关操作,是一个IO密集型任务调度器。
  • ·Main:UI调度器,根据平台不同会被初始化为对应的UI线程的调度器,例如在Android平台上它会将协程调度到UI事件循环中执行,即通常在主线程上执行。
  • ·Unconfined:“无所谓”调度器,不要求协程执行在特定线程上。它在挂起点恢复执行时会在恢复所在的线程上直接执行,当然,如果嵌套创建以它为调度器的协程,那么这些协程会在启动时被调度到协程框架内部的事件循环上,以避免出现StackOverflow。

作用域

GlobalScope‌

GlobalScope‌是有全局生命周期感知的作用域,继承自CoroutineScope。

GlobalScope‌.launch默认运行在名称为DefaultDispatcher-worker-[123..]的线程中,这个线程和主线程可能同属于名称为main的线程组。

MainScope

框架中有一个函数MainScope,它可以创建一个基于UI调度器的主从作用域,将协程作用域与UI的生命周期绑定到一起,作用域的好处就是可以方便地绑定到UI组件的生命周期上,在Activity销毁的时候直接取消,所有由该作用域启动的协程就会被取消。

class ScopedActivity:AppCompatActivity(){
private val mainScope by lazy {MainScope()}
override fun onCreate(savedInstanceState:Bundle?){
mainScope.launch{
. . .//调度到UI线程
}
}

override fun onDestroy(){
super.onDestroy()
//用完销毁
mainScope.cancel()
}
}

lifecycleScope

Android KTX为Jetpack的Lifecycle相关组件已都提供了已经绑定了UI生命周期的作用域供直接使用,添加Lifecycle相应的基础组件之后,再添加以下组件即可:androidx.lifecycle:lifecycle-runtime-ktx:2.2.0。lifecycle-runtime-ktx提供了LifecycleCoroutineScope类及其获得方式,例如可以直接在MainActivity中使用lifecycleScope来获取这个实例。这是因为MainActivity的父类实现了LifecycleOwner接口,而lifecycleScope则正是它的扩展成员。如果想要在ViewModel中使用作用域,需要再添加以下依赖:androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0,ViewModel的作用域会在它的clear函数调用时取消。

在 Android 开发中,‌lifecycleScope.launch默认在主线程(UI 线程)上执行‌。

  • lifecycleScope是 Android Jetpack 提供的生命周期感知协程作用域,绑定到ActivityFragment的生命周期。
  • 当使用lifecycleScope.launch { ... }启动协程时,‌若未显式指定调度器(dispatcher)‌,协程会继承lifecycleScope的默认上下文,即 ‌Dispatchers.Main‌,也就是主线程。
  • 这意味着协程体中的代码(如更新 UI)可以直接在主线程安全运行,但‌不适合执行耗时的 I/O 或 CPU 密集型操作‌,否则会导致主线程阻塞,引发 ANR(应用无响应)。

若需在后台线程执行任务(如网络请求、数据库操作),应使用withContext(Dispatchers.IO)Dispatchers.Default显式切换线程。

协程生命周期管理

Job 与取消协程

launch 返回的 Job 对象可管理协程:

  • job.start():启动协程(默认自动启动);
  • job.cancel():取消协程,但是它不会等待协程的结束。在调用 job.cancel() 后,协程会被标记为取消,但并不会立即终止,协程的执行会在下一个挂起点(如delay)处中止。调用此方法后,协程可能会被中断 ,但并不保证它会立刻停止,尤其是在协程处于挂起状态时。协程的取消是通过异常机制(CancellationException)实现的,协程会根据挂起点的条件来决定是否立刻取消。调用后,协程会被标记为取消,挂起点处会抛出 CancellationException,但调用线程不会等待协程的结束。
  • job.join():等待协程完成;
  • job.cancelAndJoin():取消并等待完成;会等待协程的结束。cancelAndJoin() 方法不仅会取消协程,还会等待该协程完全终止。这意味着,调用cancelAndJoin() 后,当前线程会阻塞,直到协程取消并完成。通常,当需要在取消协程之后确保它完全结束后再执行后续操作时,应该使用 cancelAndJoin()。如果只需要取消协程,不需要关心它是否完全结束,可以使用 cancel()。
  • job.isActive:判断协程是否活跃,计算密集型的协程应定期检查 isActive 或调用 yield()。

yield函数的作用主要是检查所在协程的状态,如果已经取消,则抛出取消异常予以响应。

调用job.cancel后协程状态变为isActive false,在协程体中使用coroutineContext.isActive判断取消状态。在协程内部,可以使用isActive属性来检查协程是否被取消,这样可以在执行过程中优雅地处理取消操作。在协程内部,也可以使用try-catch块来捕获CancellationException,这允许在协程被取消时执行一些清理操作。

kotlinx 协程的所有suspend函数都是可以取消的。可以通过job的isActive状态来判断协程的状态,或者检查手否有抛出 CancellationException 时取消。例如,协程正工作在循环计算中,并且不检查协程当前的状态, 那么调用cancel来取消协程将无法停止协程的运行, 有两种方法可以使计算代码取消成功。

方法一: 显式检查取消状态isActive,isActive属性是CoroutineScope中的属性。

方法二: 循环调用一个挂起函数yield(),该方法实质上是通过job的isCompleted状态值来捕获CancellationException完成取消功能。只需要在while循环体中循环调用yield()来检查该job的取消状态,如果已经被取消,那么isCompleted值将会是true,yield函数就直接抛出CancellationException异常,从而完成取消的功能。

结构化并发:CoroutineScope

CoroutineScope用于管理多个协程的生命周期,核心规则:

  • 作用域可以执行cancel。作用域取消时,所有子协程自动取消。
  • Android 中常用lifecycleScope(生命周期绑定)、viewModelScope(ViewModel 绑定)。
fun main() { // 创建自定义作用域 val scope = CoroutineScope(Dispatchers.IO) scope.launch { repeat(5) { println("协程1:$it") delay(500) } } scope.launch { repeat(5) { println("协程2:$it") delay(500) } } // 等待 1 秒后取消作用域(所有子协程取消) Thread.sleep(1000) println("取消作用域") scope.cancel() }

协程进阶用法

1. Flow:异步数据流

Flow是协程的异步数据流,替代传统回调。

import kotlinx.coroutines.flow.* fun getNumbers(): Flow<Int> = flow { for (i in 1..5) { delay(500) emit(i) // 发射数据 } } fun main() = runBlocking { getNumbers() .filter { it % 2 == 0 } .map { it * 10 } .collect { println("接收数据:$it") } } // 输出: // 接收数据:20 // 接收数据:40

2. Channel:协程间的管道

Channel允许在协程之间传递数据流

import kotlinx.coroutines.channels.Channel fun main() = runBlocking { val channel = Channel<Int>() launch { for (x in 1..5) { channel.send(x * x) } channel.close() } for (y in channel) { println(y) } }

3. 合并多个异步源

  • combine:合并两个 Flow
  • async/await:合并多个 Deferred
fun main() = runBlocking { // 合并两个 Flow val flow1 = flow { emit(1); delay(100); emit(2) } val flow2 = flow { emit("A"); delay(200); emit("B") } combine(flow1, flow2) { num, str -> "$num-$str" } .collect { println(it) } // 合并两个 async 结果 val deferred1 = async { 10 } val deferred2 = async { 20 } println("合并结果:${deferred1.await() + deferred2.await()}") }

4. 超时处理:withTimeout

设置协程执行超时时间,可以在外部捕获超时异常

fun main() = runBlocking { try { withTimeout(1000) { repeat(3) { println("执行中:$it") delay(600) } } } catch (e: TimeoutCancellationException) { println("协程超时") } }

5. SharedFlow 与 StateFlow

用于在多个收集器之间共享状态:

  • SharedFlow:可配置的事件流
  • StateFlow:持有一个可观察的状态值(类似 LiveData)

最佳实践

  • 避免使用 GlobalScope:应使用自定义作用域或结构化并发;
  • 为主线程安全设计:使用 withContext(Dispatchers.IO) 执行耗时操作;
  • 合理选择调度器:
    • UI 操作:Dispatchers.Main
    • CPU 密集型:Dispatchers.Default
    • IO 密集型:Dispatchers.IO
  • 使用超时保护:withTimeout 防止协程无限运行;
  • 适当处理取消:计算密集型的协程应定期检查 isActive 或调用 yield();
  • 使用监督作用域处理独立任务:一个子任务失败不应影响其他任务时;
  • Android 开发中协程作用域优先使用 lifecycleScope / viewModelScope。

总结

  • 核心概念:协程是轻量级并发框架,通过 suspend 实现非阻塞挂起,Dispatchers 灵活切换线程,以同步写法实现异步逻辑。
  • 基本使用:launch 启动无返回值的协程,async 返回可等待的结果,runBlocking 桥接普通代码。
  • 结构化并发:协程作用域管理协程生命周期,父子 Job 形成层次结构,自动传播取消和异常。
  • 异常处理:CoroutineExceptionHandler 捕获未处理异常,supervisorScope 改变异常传播。
  • 协程通信:Channel 用于协程间通信,Flow 处理异步数据流,StateFlow 共享状态。
  • 性能原理:挂起释放线程,非阻塞等待实现高效并发,可启动数十万协程。

掌握协程是 Kotlin 进阶开发的关键一步,它能够以直观的顺序代码处理复杂的异步场景,大幅提升运行性能和可维护性。

异步处理

异常也是函数调用结果的一种,那完全可以对抛出的异常与返回的结果一视同仁。

http://www.cnnetsun.cn/news/1774075.html

相关文章:

  • 4. 对象新增了哪些扩展?
  • 嵌入式学习笔记——认识STM32的 GPIO口
  • GPT-5.2三兄弟怎么选?Instant/Thinking/Pro保姆级对比,附Python/Node.js接入避坑指南
  • 2026年4月OpenClaw(Clawdbot)如何集成?华为云新手攻略:搭建及大模型API、Skill配置指南
  • Chatblade终极指南:10个技巧让你成为ChatGPT CLI高手
  • 2026年4月OpenClaw(Clawdbot)如何搭建?京东云快速流程:部署与大模型API、Skill集成指南
  • 实战避坑指南:用一颗12V/50A的eFuse(比如MP5991)给大功率板卡供电,我的PCB布局踩了哪些雷?
  • Lit-LLaMA与LitGPT终极对比:如何选择最适合你的大语言模型方案
  • OpenClaw健康检查:千问3.5-9B服务状态监控与告警
  • 达梦数据库图形化安装界面常见报错及解决方案
  • 算法工具箱之前缀和
  • NeMo Guardrails CLI工具终极指南:从调试到部署的完整教程
  • Spring Boot 4.3 新特性:构建更智能的 Java 应用
  • 浏览器神器Tampermonkey:手把手教你安装和使用4款必备油猴脚本
  • Sequel批量插入性能终极指南:如何快速处理百万级数据
  • MovieGuide依赖注入教程:Dagger 2在Android项目中的终极指南
  • OpenClaw+Kimi-VL-A3B-Thinking成本对比:自建多模态服务vs商用API
  • OpenClaw+Kimi-VL-A3B-Thinking成本对比:自建vs云API哪种更划算
  • Embree错误处理与调试:常见问题排查与解决方案
  • 终极指南:如何为Tech-Interview-Cheat-Sheet开源项目贡献代码
  • 从“找茬”到“预防”:AI如何预测代码中的潜在Bug
  • C++实现字符串转整数(atoi)详解
  • OpenClaw安全实践:Qwen3.5-9B本地化处理敏感数据
  • Phi-4-mini-reasoning开发者指南:日志排查、健康检查与服务重启实操
  • 告别卡顿!香橙派PC刷入Ubuntu 22.04 LTS,保姆级从烧录到EMMC迁移全流程
  • Globby最佳实践:避免常见陷阱的7个技巧
  • 基于ESP32S3芯片的机器人控制器设计与实现
  • # 发散创新:基于事件驱动架构的实时日志监控系统设计与实现在现代分布式系统中,**事件驱动编程模型**正
  • OpenClaw开源贡献:为Qwen3.5-9B-AWQ-4bit编写自定义技能指南
  • RockyLinux 8.6安装与Linux核心命令掌握(2/2)