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 函数中,或者用于测试用例代码中。还可以使用withTimeout和withTimeoutOrNull来限制执行时间并优雅地取消协程。
协程的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句)
总结
- 异步无返回、需独立控制 → 用 launch(Dispatchers.IO)
- 同步需返回、要自动切回原线程 → 用 withContext(Dispatchers.IO)
- 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 提供的生命周期感知协程作用域,绑定到Activity或Fragment的生命周期。- 当使用
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 // 接收数据:402. 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 进阶开发的关键一步,它能够以直观的顺序代码处理复杂的异步场景,大幅提升运行性能和可维护性。
异步处理
异常也是函数调用结果的一种,那完全可以对抛出的异常与返回的结果一视同仁。
