一个常见面试问题:Kotlin 协程能够完全取代线程吗?为什么?

3,480 阅读5分钟

某种程度上考虑「Kotlin 协程确实足够直接取代线程」,但是「协程能够完全取代线程」的说法其实不太准确,毕竟协程是必须基于线程,所以线程肯定是需要存在的,更准确的说,应该是 Kotlin 协程在 Android 上可以取代大部份线程场景

当然,这里我们只讨论 Androd 平台上的协程,这个问题的答案也需要约定在 Android 平台,因为 Kotlin 有 KMP 等多平台需求存在,比如 :

  • Kotlin/JS 上和 Kotlin/JVM 在协程上还存在一些差别,编译后在 JS 平台会是单线程下的 Promise\setTimeout 状态机模式
  • iOS 上通过 SKIE 将 Kotlin flow 适配 Swift AsyncSequence,另外 Kotlin 2.1 开始尝试 swift export ,也许后续会直接支持 Swift 的异步协程模型、它和 Kotlin 的协程又不大一样

在 Swift 官方文档里就提到:When an asynchronous function resumes, Swift doesn’t make any guarantee about which thread that function will run on.

事实上现在很多语言都在弱化开发者直接操作线程的情况,比如 :

  • Swift 中的并发模型构建在线程之上,但开发者不会直接与它们交互
  • Dart 是运行在独立 isolate 的异步协程模式,但是每个 isolate 并不代表系统线程单位

所以 Kotlin 的协程,在日常开发里,确实可以在大部份时候取代线程,这也是 Jetbrains 所希望的的结果。

那为什么可以呢?

首先简单介绍为什么会有协程,Kotlin 协程核心的目的之一就是:轻量化。

那为什么说协程轻量化,因为在线程场景下,我们要同时运行多个异步任务,我们大概率需要开多个线程来运行,比如 JVM 上我们会开一个线程池来实现,也就是实际上对于线程的开启和销毁大概率会有一定浪费。

而有了协程之后,可以把协程当作是线程上的一个任务单元,也就是可以在一个线程上运行多个任务,这样一定程度在实现异步操作的同时,可以减少线程的消耗,并且更充分使用线程资源,比如:

大部分时候你只是需要一个异步 IO 操作,而并不是要完全「多并发」的线程操作。

那么协程的概念在这时候的作用就比较明显了,通过在一个线程上支持挂起/恢复的操作,然后在任务完成回调后恢复,这样就可以让一个线程同时支持多个异步任务,从而充分利用线程,并减少软件运行过程中的线程数量和开销。

v2-fd8aad1d40bd63ce08e3aca0b051df9f_720w.webp

当然,Kotlin 里的协程概念又和某些语言不大一样,因为它并不在同一个线程运行,或者说并不一定在同一个线程,在官方描述里,"a coroutine is not bound to any particular thread" ,它更多是一种调度器(Dispatcher)的机制,也就是 Kotlin 里协程不绑定到任何特定线程,它可能会在一个线程中暂停执行,并在另一个线程中恢复执行。

也就是,Kotlin 协程是真的会创建多个线程。

所以 Kotlin 里的协程可以被认为是「轻量级线程池」模型,只是协程的资源密集度低于 JVM 线程,例如官方的示例代码,以下代码启动 50000 个不同的协程,每个协程等待 5 秒,然后输出一个句点 ('.'),实际运行中会占用很少的内存:

import kotlinx.coroutines.*
​
fun main() = runBlocking {
    repeat(50_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

如果使用线程实现(删除runBlocking,将launch替换为thread,将delay替换为Thread.sleep),它会消耗大量内存,甚至可能会引发内存不足错误或缓慢启动线程。

如果在实际案例里面,比如多个网络请求,就可以在一个线程上体现为下图所示:

image.png

那么为什么前面说的是绝大部分场景下呢? 因为总有某些 CPU 密集型操作,例如音视频、图像处理领域,或者其他需要大数据密集型的并发操作,而这些操作任务会占用 CPU 时间,不能轻易地被挂起或分解成「协程颗粒」的异步任务,甚至需要精确的线程调度等底层并发控制时,还是直接操作线程池更可控:

毕竟,简单思考下,线程一旦开始可以直接执行到任务结束,而这个过程还都是连续可控的。

当然,对于 Android 开发来说,日常 kotlin 协程完全可以替代以前 Java 时代的线程池操作,大部分时候,你只需要直接使用协程就够了。

当然,这里面还有另一个角度的思考,我们不考虑线程or协程的性能优势,而是考虑项目应该被构建为使用阻塞代码还是异步代码?你是否需要「结构化并发」的写法:基于代码块,作用域,实现代码的抽象和封装,并且异步任务存在父子关系,需要实现单一入口和单一出口的结构。

如果你的异步场景,存在子任务和主任务之间之间的时序关系,从这个业务角度,那么事实上协程确实就是天然优越于线程模型。

另外,我们前面反复说 Kotlin 协程 ,其实也是为了区别其他语言,比如 Flutter 下的 Dart ,默认情况下 Dart 的异步模式,这种协程它真的就在一个线程,也就是如果你不开启新的 isolate(当然,isolate 也不就一定是线程完全挂钩) , Dart 默认跑在单线程循环的任务调度模型,那么这种情况,单凭协程肯定是无法满足需求,这也是和 Kotlin 协程在概念上的区别。

image.png

所以,协程的概念,和 Kotlin 协程的具体实现,本身还有区别,不同语言在协程处理上也存在部份差异理解,当然本质上目的还是采用轻量化的并发操作,来避免开发者直接操作线程,同时提高线程的资源利用。