资讯动态

Kotlin 协程基础一 —— 总体基本概述

发布时间:2026/10/27 9:36:41 来源:尧图企业网站定制
1. 前言本系列文章会以Kotlin协程的基础讲解介绍协程的概念、用法与适用场景此外还会介绍与协程紧密相关的 Flow 的相关知识。首篇会先介绍协程的基本概念与使用方法。需要先声明的是由于不同平台对 Kotlin 协程的底层实现是不一样的对于 Android 开发者而言我们主要谈论的是针对 JVM 上的 Kotlin 协程。2. 协程的作用先来了解一下协程是什么协程本来是与线程并列的概念都是并发管理工具。只不过有的语言用线程如 Java管理并发有的用协程如 Kotlin有的二者都用Kotlin 协程实际上是将 Java 线程封装成一套新的 API 来管理并发。因为 Kotlin 的特殊性在于它是一门中间语言其代码最终还是要编译成 Java 字节码前面说过我们研究的是 JVM 的 Kotlin 协程所以 Kotlin 是无法绕过线程来创建新的并发实现的为什么已经有了线程还要用协程一句话概括协程更好用最直接的体现就是可以用线性结构来写并发代码。比如传统方式为了执行一个耗时任务需要在子线程中运行该耗时任务拿到结果后需要先切回主线程再通过接口回调把执行结果传给主线程调用处。但是使用协程可以通过线性结构的代码就完成上述任务无需使用回调代码。实际上协程所有的优点都是基于“可以用线性结构来写并发代码”这一点的比如协程比线程轻协程实际上是基于线程的怎么可能比线程轻只不过是因为写法简单了、结构化并发等等。关于“协程比线程轻”这种说法很常见因为 Kotlin 官方文档介绍协程时就说了“协程是轻量级的线程”协程比 JVM 线程更省资源。这里可以理解为协程可以用更简单的代码达到与使用线程相同的效果。3. 线程的切换前面提到协程与线程都是并发管理工具而并发管理涉及到的无外乎三件事线程切换先切换到子线程执行耗时任务得到任务结果后再切回主线程传回任务结果线程同步在各个线程执行的过程中等待别的线程属于线程之间的流程配合线程安全通过互斥锁等机制保护线程间的共享资源无论从线程还是协程的角度都是这三件事我们先说最基本的 —— 切线程。当然由于协程与线程有着千丝万缕的关系在介绍协程时通常我们会用线程对比的方式来进行。3.1 传统切线程的方式传统方式执行后台任务通常都是启动一个子协程thread { ... }或者使用线程池val executor Executors.newCachedThreadPool() executor.execute()对于 Android 客户端而言在后台线程执行完毕拿到结果后需要切回到主线程更新 UI此时可以使用主线程的 Handler 或 View 进行切换val handler Handler(Looper.getMainLooper()) handler.post { } val view: View findViewById(R.id.xxx) view.post { }3.2 启动协程使用协程需要添加协程依赖如果你是做服务端的那么添加协程的核心依赖即可implementation org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1对于 Android 开发而言还需再加一项implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1最基本的协程启动方式// EmptyCoroutineContext 是一个空的协程上下文CoroutineContext 后续会详解 val scope CoroutineScope(EmptyCoroutineContext) scope.launch { }这样会使用默认线程执行任务当然协程通过“管理任务执行的线程”的工具 —— ContinuationInterceptor 来进行线程切换。Kotlin 官方提供了 4 个实现类来帮助我们完成“切线程”的任务/** * 4 个实现类来自两种类型CoroutineDispatcher 和 MainCoroutineDispatcher。 * 抽象类 CoroutineDispatcher 实现了接口 ContinuationInterceptor * 而 MainCoroutineDispatcher 继承 CoroutineDispatcher也是一个抽象类。 */ public actual object Dispatchers { // 默认调度器用于处理计算密集型任务 JvmStatic public actual val Default: CoroutineDispatcher DefaultScheduler // 切到主线程执行任务 JvmStatic public actual val Main: MainCoroutineDispatcher get() MainDispatcherLoader.dispatcher // 不限制引申为不进行线程管理。具体表现在启动协程时不会切线程在挂起函数执行完之后也不会自动 // 把线程切回去其他三个会而是继续在挂起函数所在的线程执行后续代码。实际开发中几乎不会用到 JvmStatic public actual val Unconfined: CoroutineDispatcher kotlinx.coroutines.Unconfined // 用于处理 IO 密集型任务 JvmStatic public val IO: CoroutineDispatcher DefaultIoScheduler }Default 与 IO 的主要区别在于它们使用的线程池的大小Default 使用的线程池的核心线程数大小为 CPU 的核心数。即对于 N 核心的 CPUDefault 会创建一个核心线程数为 N 的线程池。不需要更多的线程因为数量相等时已经可以让每个核心都跑一个线程了实现满载。如果创建更多线程会在某个核心上出现线程切换我们知道线程的上下文切换的开销是很大的这样效率不仅没有提高反而降低了IO 在一般情况下核心数不超过 64会开 64 个线程如果 CPU 核心数大于 64那么就有多少核心开多少线程。之所以开这么多是因为 IO 密集型任务的主要工作都在磁盘或网络执行CPU 只是在开启任务和接收结果时会做一些工作因此 CPU 在整个 IO 工作过程中是很闲的。为了充分利用 CPU在 IO 密集型任务中可以多开一些线程让 CPU 得以充分利用开启协程时可以在 CoroutineScope 或 launch() 传入指定的调度器// 1.默认情况使用 Default val scope CoroutineScope(EmptyCoroutineContext) scope.launch { } // 2.对所有通过 scope1 启动的协程使用 IO 线程池 val scope1 CoroutineScope(Dispatchers.IO) scope1.launch { } scope1.launch { } // 3.仅对传入 IO 的 launch 使用 IO 线程池对 scope2 下其他 // 未指明的 launch 使用 Default val scope2 CoroutineScope(EmptyCoroutineContext) scope2.launch(Dispatchers.IO) { }3.3 自定义线程池除了系统提供的 4 个调度器使用者也可以自定义线程池上下文供协程使用。比如通过 newFixedThreadPoolContext() 创建一个固定数量的线程池的上下文DelicateCoroutinesApi public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher { require(nThreads 1) { Expected at least one thread, but $nThreads specified } val threadNo AtomicInteger() val executor Executors.newScheduledThreadPool(nThreads) { runnable - val t Thread(runnable, if (nThreads 1) name else name - threadNo.incrementAndGet()) t.isDaemon true t } return executor.asCoroutineDispatcher() }将这个上下文传给 CoroutineScope 再启动协程val context newFixedThreadPoolContext(4, MyThread) val scope CoroutineScope(context) scope.launch {}注意 newFixedThreadPoolContext() 被标记为 DelicateCoroutinesApidelicate 译为“虚弱的、易碎的”目的是提醒我们使用它容易犯错易错点就在于返回值类型 ExecutorCoroutineDispatcher 上public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable { /** suppress */ ExperimentalStdlibApi public companion object Key : AbstractCoroutineContextKeyCoroutineDispatcher, ExecutorCoroutineDispatcher( CoroutineDispatcher, { it as? ExecutorCoroutineDispatcher }) /** * Underlying executor of current [CoroutineDispatcher]. */ public abstract val executor: Executor /** * Closes this coroutine dispatcher and shuts down its executor. * * It may throw an exception if this dispatcher is global and cannot be closed. */ public abstract override fun close() }实现了 Closeable 的对象在使用完毕后需要调用 close() 进行关闭这一点容易被使用者忽略因此才打了 DelicateCoroutinesApi 注解提醒使用者。那为什么 Dispatchers 中提供的 Default 和 IO 不用手动关闭呢因为那些是全局的永久存活不需要使用者手动关闭。与 newFixedThreadPoolContext() 类似的还有一个 API newSingleThreadContext()DelicateCoroutinesApi public actual fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher newFixedThreadPoolContext(1, name)3.4 withContext() 手动切线程前面讲过的 launch() 以及后续要讲的 async() 都是并行切线程而 withContext() 则是串行切线程。举个例子CoroutineScope(Dispatchers.Main).launch { // 1 launch(Dispatchers.IO) { delay(100) // 2 } // 3 }最外面用 CoroutineScope 的 launch() 启动的协程我们称为父协程在里面用 launch() 启动的协程我们称为子协程。子协程启动后父协程会继续执行后续代码而不会等待子协程内容执行完再继续。因此上述代码的执行顺序为 1 - 3 - 2。launch() 就是一个协程的启动器它将协程启动后launch() 使命也就结束了其 lambda 代码块中的代码会由协程负责执行launch() 不会等到协程运行完这些代码再结束而是在启动协程后就结束了。使用 withContext() 这个挂起函数可以达到串行执行的效果CoroutineScope(Dispatchers.Main).launch { // 1 withContext(Dispatchers.IO) { // 2 } // 3 }由于挂起函数在运行完之前协程不会运行内部的其他内容因此此处的执行顺序为 1 - 2 - 3。我们马上就要介绍挂起函数但是这里先提一下withContext() 能实现串行切线程的本质。withContext() 能实现串行切协程并不是这个函数单独实现了某种特殊功能而是因为它是一个挂起函数。挂起函数的特性就是在运行时协程需要等待该挂起函数运行完毕后才能继续执行后续内容也就是协程在运行挂起函数时被挂起了运行完毕后再恢复。所以才能达到串行切协程的效果。需要注意区分的是协程被挂起时会让出当前线程的执行权让该线程运行其他协程的内容。因此协程挂起不会阻塞线程不会影响线程的运行。4. 挂起函数4.1 让线程自动切回来在后台线程执行完任务自动切回原线程是通过挂起函数实现的。挂起函数的特性是把协程与线程暂时分离在挂起函数结束之后再切回到线程因此挂起函数只能在协程中调用才有意义。挂起函数通过自动切回原来的线程消除了回调这样就简化了代码结构从而大大降低了写并发代码的难度。比如说通过 Retrofit 发送网络请求传统方式需要通过回调切换回原来的线程并且如果有需求需要拿到前一次的结果之后再发出二次请求还会有嵌套回调甚至回调地狱的情况发生const val GITHUB_API https://api.github.com data class Contributor(val login: String, val contributions: Int) interface GitHub { GET(/repos/{owner}/{repo}/contributors) fun contributorsCall( Path(owner) owner: String, Path(repo) repo: String ): CallListContributor }执行异步任务代码如下private fun callbackStyle() { gitHub.contributorsCall(square, retrofit) // enqueue 会进入子线程发送网络请求在拿到结果通过回调返回 // 结果之前再切回到当前线程之中 .enqueue(object : CallbackListContributor { override fun onResponse( call: CallListContributor, contributors: retrofit2.ResponseListContributor ) { // 倘若这里需要利用 contributors 发出二次请求那么就会出现嵌套的回调 showContributors(contributors) } override fun onFailure(p0: CallListContributor, p1: Throwable) { TODO(Not yet implemented) } }) }假如使用挂起函数与协程interface GitHub { // 返回值直接就是数据而不是 CallListContributor GET(/repos/{owner}/{repo}/contributors) suspend fun contributors( Path(owner) owner: String, Path(repo) repo: String ): ListContributor }执行请求发现代码大大减少了private fun coroutineStyle() CoroutineScope(Dispatchers.Main).launch { // 在子线程中执行 gitHub.contributors()执行完毕后回到主线程赋值然后进行后续代码 val contributors gitHub.contributors(square, retrofit) showContributors(contributors) }执行挂起函数的协程会被暂停让出所在的线程与该线程暂时脱离。此时挂起函数切换到 launch() 指定的线程在该线程中执行网络请求完成后切换回原来的线程协程恢复resume执行后续代码。按照这种说法上面的例子contributors() 这个挂起函数实际上也是在主线程中启动的只不过由于 Retrofit 对 Kotlin 的挂起函数有一个优化它会将挂起函数中的网络请求通过适配器 SuspendForBody 转换成一个通过 OkHttp 的线程池中的子线程发出的请求。相当于框架帮助使用者实现了线程切换无需在执行网络请求前手动切换线程了。由于挂起函数会挂起协程因此挂起函数在协程里调用才有意义。或者在其他挂起函数中被调用间接的在协程中被调用。所以才有挂起函数只能在协程或其他挂起函数中调用的规则。由于挂起函数能自动切回原线程因此就不用再回调了从而简化了代码形式。4.2 自定义挂起函数使用 suspend 关键字即可定义挂起函数问题是什么时候需要把一个函数写成挂起函数呢通常是内部会调用到其他挂起函数的时候有点“被迫”地声明为挂起函数的意味。这是因为挂起函数只能在协程或其他挂起函数内调用正是因为这个限制使得挂起函数的使用范围收窄了在你需要调用到其他挂起函数时为了满足这一限制才需要把自己也声明为挂起函数。除此之外无需主动的将函数声明为挂起函数因为如果内部没有调用挂起函数却主动将自己声明为挂起函数反而会导致自己的使用范围变窄。写代码的时候不用特别关注哪些函数应该写成挂起函数IDE 让你 suspend 的时候加上就可以了。这是被动的要求而不是主动的需求。4.3 性能优势协程相比于线程的性能优势体现在协程的机制上。先来看一个示例代码CoroutineScope(Dispatchers.Main).launch { val data withContext(Dispatchers.IO) { // 网络数据 data } // 数据计算交给 Default val processedData withContext(Dispatchers.Default) { processed $data } // 最终结果在主线程中呈现比如显示到 UI 上 println(Processed data: $processedData) }思考一个问题假如想把得到 processedData 变量的处理过程抽取出来是应该只抽取处理数据的逻辑本身private fun getProcessData1(data: String) processed $data还是应该连同 withContext() 一起抽取private suspend fun getProcessData2(data: String) withContext(Dispatchers.Default) { processed $data }之所以有此疑问是因为 withContext() 并不面向具体业务并不像一般的挂起函数那样拥有具体的业务功能interface GitHub { GET(/repos/{owner}/{repo}/contributors) suspend fun contributors( Path(owner) owner: String, Path(repo) repo: String ): ListContributor }可以使得在使用与抽取 contributors() 时毫不犹豫的加上 suspend。答案是应该带着 withContext() 抽取。原因如下首先一段代码在哪种协程环境下执行实际上是已经绑定了的。比如说 processed $data 它模拟的就是经过大量计算才得出的结果也就是计算密集型任务它理应就在 Default 中执行。所以带着 withContext() 抽取是直接将该代码的使用环境一起封装了这样调用端就不用考虑切线程的问题了也能防止调用端忘记切线程如果带着 withContext() 抽取假如调用端将其放在同样的线程环境下比如CoroutineScope(Dispatchers.Default).launch { getProcessData2(data) }那么会出现已经在 Default 环境下在 getProcessData2() 内部又通过 withContext() 切换到 Default 的情况是否会有性能损失没有协程做了针对性优化如果发现在切换时 ContinuationInterceptor 没有改变那么就不会切换线程而是保持在原线程继续执行代码。也就是说抽取 withContext() 没有什么坏处带着 withContext() 封装函数实际上是系统设计层面、语法层面提供的一种优势。因为任何耗时操作属于那种类型在一开始就已经确定了。比如网络访问就是 IO 密集型数据处理就是计算密集型。那么在封装函数时将耗时操作通过 withContext() 放到对应的 ContinuationInterceptor 当中就能保证它们在正确的线程或线程池中执行。这样做能提升软件性能因为以往很大一部分的性能问题就是因为一些耗时操作没能放到它应该放入的线程中执行造成的比如看似不像耗时操作的耗时操作放在主线程了。以前线程受语法特性限制只能通过函数创建者以注释的方式来提醒函数的调用者应该在子线程中调用无法完全避免开发者不遵从或忽略掉这些提醒的问题。而挂起函数由于语法设计的优势可以让函数的创建者直接在函数内部把线程锁死。这样让负责线程管理的人从使用者变为函数的创建者从根源上解决了性能问题。总结起来协程利用语法上的优势帮助我们可以百分百确保耗时操作的工作一定在正确线程中执行。这相对于线程 API 在系统设计层面是一个巨大的优势。因此对于如下的代码CoroutineScope(Dispatchers.Main).launch { val data withContext(Dispatchers.IO) { // 网络数据 data } // 数据计算交给 Default val processedData withContext(Dispatchers.Default) { processed $data } // 最终结果在主线程中呈现比如显示到 UI 上 println(Processed data: $processedData) }应该改造为如下方式项目中也常用这种方式CoroutineScope(Dispatchers.Main).launch { val data getData() val processedData getProcessData(data) println(Processed data: $processedData) } private suspend fun getData() withContext(Dispatchers.IO) { data } private suspend fun getProcessData(data: String) withContext(Dispatchers.Default) { processed $data }4.4 为什么不卡线程协程底层从子线程切换回主线程实际上还是用的主线程 Handler.post()// HandlerDispather.kt 中的 HandlerContext 类 override fun dispatch(context: CoroutineContext, block: Runnable) { if (!handler.post(block)) { cancelOnRejection(context, block) } }协程在执行挂起函数之前和之后会在状态机上进行切换。执行之前切换到指定线程执行之后切换回原来的线程。协程内部通过回调的形式帮助使用者自动完成了线程的切换因此挂起函数不会阻塞当前线程。举个例子前面有通过 Retrofit 发送网络请求的代码private fun coroutineStyle() lifecycleScope.launch { val contributors gitHub.contributors(square, retrofit) showContributors(contributors) }contributors() 是挂起函数在开始执行它之前协程会先切换到子线程中然后执行 contributors() 的任务内容执行完毕后再通过回调一般是 Handler.post()切换回原本线程最后再执行赋值以及后续的操作。也就是说contributors() 执行完毕之后才把后续任务交给 Handler.post() 发送到指定线程中Handler 拿到消息之后才会继续执行后续代码所以赋值等后续操作会在 contributors() 执行完毕后再执行但是挂起函数并没有阻塞当前线程因为挂起函数实际上是在另一个子线程中执行的当然不会阻塞当前线程了。总结挂起函数在执行前后实际上是通过状态机进行了线程切换的执行前切到指定线程执行后通过回调切回原线程。原线程并没有等待挂起函数执行因此挂起函数不卡线程。5、Android 项目中协程的写法前面举例的 coroutineStyle() 就是一个典型的 Android 协程的写法private fun coroutineStyle() CoroutineScope(Dispatchers.Main).launch { val contributors gitHub.contributors(square, retrofit) showContributors(contributors) }Dispatchers.Main 会指定在主线程中启动协程这样的话launch() 内的非挂起函数代码就都会在主线程中执行。对于 Android 开发而言KTX 库提供了很多扩展属性可以结合实际场景替代 CoroutineScope。比如 Lifecycle 库提供的LifecycleOwner的扩展属性 lifecycleScopepublic val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() lifecycle.coroutineScope由于 LifecycleOwner 接口的实现类 ComponentActivity 和 Fragment 基本上会作为所有 Activity 和 Fragment 的基类因此 lifecycleScope 就适用于 Activity 和 Fragment它会与 Activity 或 Fragment 进行生命周期的绑定在其生命周期结束时如 Activity.onDestroy()自动取消 CoroutineScope 包含的所有协程。lifecycleScope 内置了 Dispatchers.Main.immediate 使得其默认在主线程中启动协程public val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { val existing mInternalScopeRef.get() as LifecycleCoroutineScopeImpl? if (existing ! null) { return existing } val newScope LifecycleCoroutineScopeImpl( this, SupervisorJob() Dispatchers.Main.immediate ) if (mInternalScopeRef.compareAndSet(null, newScope)) { newScope.register() return newScope } } }Dispatchers.Main.immediate 相比于 Dispatchers.Main 做了一些优化后者是在任何情况下都使用 Handler.post() 将任务切换到主线程而前者则会进行一次判断只有在当前不在主线程时才通过 Handler.post() 切回主线程否则就直接开始执行后续代码了有一定的性能优化作用。有时我们不直接使用 Activity 或 Fragment 来管理数据而是使用 ViewModelKTX 也提供了对应的 Scope即 viewModelScope使用前需先导入依赖implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7导入后可以在 ViewModel 范围内使用 viewModelScopeclass MyViewModel : ViewModel() { fun test() { viewModelScope.launch { } } }viewModelScope 是 ViewModel 的扩展属性public val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope? this.getTag(JOB_KEY) if (scope ! null) { return scope } return setTagIfAbsent( JOB_KEY, CloseableCoroutineScope(SupervisorJob() Dispatchers.Main.immediate) ) }你可以看到其内部也使用了Dispatchers.Main.immediate。6、结构化并发结构化并发作为协程中一个很宏大的概念会在系列的下两篇文章中讲解这里只是先提一下概念。挂起函数之所以关键是因为它简化了并发任务的代码结构使得用协程写出的并发代码比线程的更好写也更好读它面向的是并发任务的写法。而结构化并发面向的是任务的管理比如协程的取消与异常管理。CoroutineScope 可以启动协程因为它提供了协程运行的环境 CoroutineContext比如前面提到的 ContinuationInterceptor以及后续会讲到的 CoroutineName、CoroutineExceptionHandler 等。除此之外CoroutineScope 的 cancel() 可以取消所有由该 CoroutineScope 启动的协程也可以通过 Job.cancel() 取消由某一个 launch() 启动的协程。协程取消可以有效的避免内存泄漏在某些代码流程即将走完之时应该取消协程。当然像 LifecycleScope 以及 ViewModelScope 这种与特殊组件绑定的 CoroutineScope它们会在组件生命周期结束时自动调用 cancel() 结束所有协程以避免内存泄漏。协程的结构化并发不仅仅是一对多指父协程可以有多个子协程也可以是一层对多层的指父协程内可以有多层子协程。结构化是指协程具备的一层层的结构化关系而结构化并发就是对这种结构的管理。具体来说就是流程上的相互配合以及在协程取消和遇到异常时的结构化合作。7、并行协程的启动和交互前面已经讲过两种切线程的方式 —— launch() 与挂起函数。从代码运行角度来说launch() 启动多个协程时它们之间是并行关系// 两个协程并行运行 CoroutineScope(Dispatchers.Main).launch { } CoroutineScope(Dispatchers.Main).launch { }而挂起函数是串行private fun coroutineStyle() lifecycleScope.launch { val contributors gitHub.contributors(square, retrofit) showContributors(contributors) }对 contributors 变量的赋值以及后续操作需要等待挂起函数执行完毕再按顺序执行。虽然挂起函数与为 contributors 赋值的操作不在同一个线程挂起函数运行在子线程而赋值操作则是在 lifecycleScope 绑定的主线程中执行但是这之中隐含了线程之间的交互。那么两个并行的协程如何交互呢可以通过 async() 和 await() 以及 join()。比如两个并行的协程需要将运行结果相加// async 启动的协程有返回结果 Deferred val deferred1 lifecycleScope.async { delay(300) 20 } val deferred2 lifecycleScope.async { delay(500) 30 } lifecycleScope.launch { // 1.通过 await() 能拿到 async 开启的协程的返回结果 // 2.两个协程并行执行这里耗时是二者较长的一个而不是总和 deferred1.await() deferred2.await() }上述代码可以化简lifecycleScope.launch { // 为了让两个协程作为 launch 的子协程以便管理所以 async 前就没有再加 lifecycleScope // 否则这两个协程就是 lifecycleScope 的子协程 val deferred1 async { delay(300) 20 } val deferred2 async { delay(500) 30 } deferred1.await() deferred2.await() }还可以进一步优化lifecycleScope.launch { coroutineScope { val deferred1 async { delay(300) 20 } val deferred2 async { delay(500) 30 } deferred1.await() deferred2.await() } }用 coroutineScope 包一层能提供协程的异常的结构化管理很大方便具体原因在介绍结构化并发的异常处理部分时再详细介绍。如果希望几个协程在执行顺序上有依赖但不依赖于彼此的结果可以使用 join()。比如某些数据处理工作需要在初始化工作完成之后才能执行那么lifecycleScope.launch { // 初始化放在子协程中 val initJob launch { init() } // 获取网络数据 val data gitHub.data() // 执行 processData 之前需要初始化已经完成此时对 initJob 使用 join initJob.join() processData(data) }8、协程与线程交互由于老项目或者一些尚未支持协程的第三方库可能还是使用线程进行并发管理的假如我们在新项目中使用协程要用到线程的并发功能就需要在协程中接入线程代码反之老项目无法使用协程只能用线程的情况下但第三方库只提供了协程的并发支持此时就需要在线程中接入协程。8.1 连接线程世界先来看如何将线程代码接入协程。现有通过线程 回调方式完成 Retrofit 网络请求的代码private fun request() { gitHub.contributorsCall(square, retrofit) // enqueue 会进入子线程发送网络请求在拿到结果通过回调返回 // 结果之前再切回到主线程之中 .enqueue(object : CallbackListContributor { override fun onResponse( call: CallListContributor, contributors: retrofit2.ResponseListContributor ) { // 倘若这里需要利用 contributors 发出二次请求那么就会出现嵌套回调 showContributors(contributors) } override fun onFailure(p0: CallListContributor, p1: Throwable) { TODO(Not yet implemented) } }) }使用 suspendCoroutine 包裹上面的函数体可以将其改造为挂起函数private suspend fun callbackToSuspend() suspendCoroutine { gitHub.contributorsCall(square, retrofit) .enqueue(object : CallbackListContributor { override fun onResponse( call: CallListContributor, contributors: retrofit2.ResponseListContributor ) { // 1.传回数据 it.resume(response.body()!!) } override fun onFailure(p0: CallListContributor, p1: Throwable) { // 2.如果发生异常会结束 suspendCoroutine 并抛出 t 这个异常 it.resumeWithException(t) } }) }调用 callbackToSuspend() 时要为其加上 try-catchoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) lifecycleScope.launch { try { val contributors callbackToSuspend() showContributors(contributors) } catch (e: Exception) { // 异常处理 } } }注意这个异常捕获不能写成如下的形式lifecycleScope.launch { try { // launch 是协程的启动器在它启动协程之后launch 本社就结束了。launch 代码块的内容 // 由协程负责执行。因此launch 内部执行代码发生异常的话这样 try-catch 是捕获不到的 launch { val contributors callbackToSuspend() showContributors(contributors) } } catch (e: Exception) { // 异常处理 } }相关细则在后续结构化并发的协程异常部分会讲到。除了 suspendCoroutine()还有 suspendCancellableCoroutine()唯一区别是后者支持取消。由于取消协程是一个正常的需求大多数时候我们都希望可以取消协程因此我们通常会使用 suspendCancellableCoroutine()。在取消时可以通过 invokeOnCancellation() 这个回调函数做一些取消时的收尾工作suspendCancellableCoroutine { // 注册取消时的回调函数 it.invokeOnCancellation { ... } }注意这种取消需要协程配合也就是所谓的“协作式取消”。意思是除了在协程外部调用取消方法之外协程内部也应做出配合检查相关标记位并在做完收尾清理工作后结束协程内外协作让协程顺利取消。具体内容会在下一篇讲解结构化并发时说明。8.2 回到线程世界前面已经讲过两个协程自带的启动协程的函数 launch() 和 async()实际上还有一个 runBlocking()它有两点特殊无需 CoroutineScope 就可以启动协程会阻塞当前线程之所以有这两点是因为它的定位与另外两个不同。它的定位是将挂起式的代码转换成阻塞式的。实际上就是与上一节用协程调用线程代码刚好反过来虽然不常用但确实有需求它用于在线程环境中调用协程封装的代码。比如fun main() runBlocking { val contributors gitHub.contributors(square, retrofit) }runBlocking() 开启了一个会阻塞当前线程的协程环境在其内部执行挂起函数 contributors() 获取相关数据。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑