首先我们先创建一个新的项目,如果不知道怎么创建,先参考 学习基础环境准备 这篇文章。
Kotlin 本身是带有基础的协程功能的,但是这篇里会用到一些协程的扩展函数,所以需要加上扩展库,添加后的 build.gradle 文件如下:

  1. plugins {
  2. id 'org.jetbrains.kotlin.jvm' version '1.5.30'
  3. id 'java'
  4. }
  5. group 'org.example'
  6. version '1.0-SNAPSHOT'
  7. repositories {
  8. mavenCentral()
  9. mavenCentral()
  10. }
  11. dependencies {
  12. implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.5.21'
  13. implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2-native-mt'
  14. }
  15. test {
  16. useJUnitPlatform()
  17. }

协程是什么


在 Kotlin For Jvm 中协程是依附于线程的,线程是依附于进程的,如下图:
入门基础 - 图1

线程是系统级别,由系统调度
协程是程序级别,由程序实现创建和调度

所以可以概括协程是一个更轻量级的(相比于线程)异步框架

上边的概念,因具体实现的语言而定,最起码在定义上概念是没有多大出入的

HelloWord


创建项目后,我们先写个协程项目,然后运行下:

  1. import kotlinx.coroutines.*
  2. @DelicateCoroutinesApi
  3. fun main() {
  4. coroutineTest()
  5. println("program finish")
  6. }
  7. @DelicateCoroutinesApi
  8. fun coroutineTest() {
  9. GlobalScope.launch {
  10. println("in coroutine")
  11. }
  12. }
  13. fun threadTest(){
  14. thread(name = "testThread") {
  15. println("in thread")
  16. }
  17. }

你有可能想问 GlobalScope.launch{ … } 是什么,这个可以先忽略,可以暂时理解为 thread{ … }
运行后,查看输出,输出了一行 program finish….恩,恩?不对阿,没有输出 in coroutine ,协程创建失败了,还是我代码有问题?把 coroutineTest 换成 threadTest 试试,额..正确输出了
image.png
所以这是为什么??

其实从这个小例子里就能看出来协程和线程从根本上是不同的,至于为什么的话,这里先把问题抛出,毕竟这篇是入门基础,希望你在深入的剖析了协程后,可以还记得这个问题,然后运用你所学的知识武器攻破它。

那么转过头来,眼下这个问题怎么解决呢,我先剧透点信息因为你的主程序先于协程结束了,所以协程体内的代码不会执行。那么只要想办法让主程序不结束不就好了,这个方式有很多,这里我随便写一个 loop 函数,你也可以用自己的方式实现,例如更加 Kotlin 的写法 runBlocking { … }

  1. fun loop() {
  2. thread {
  3. while (true) {
  4. Thread.sleep(1000)
  5. }
  6. }
  7. }

现在我们把 loop 函数放在 main 函数的第一行,然后再次运行
image.png
终于成功输出了,那么你的第一个协程程序就完成了。

挂起


挂起跟阻塞有相似之处,但是却是完全不同
你问什么是阻塞?Thread.sleep(..)、网络请求啥的阻塞当前线程运行的,都叫阻塞

挂起与阻塞

在网上查询挂起和阻塞的相关概念能查到一堆专业的解答,而我才学疏浅一个没看懂… …
其实没懂也不至于,只是不知道怎么归纳的简单容易理解,所以这里我只能用现实中的例子来说明下阻塞和挂起的区别,例如下面的场景:

现在时间 10:30,你想要在 11:00 的时候喝水,这时你把你自己比作一个线程

  • 阻塞:10:30~11:00 之间,你啥也不干,就等着到点,然后立马喝水
  • 挂起:你定了个 11:00 的闹钟,在10:30~11:00 之间你有可能打代码,有可能摸鱼,有可能巴拉巴拉…等闹钟一响,立马喝水

下面使用代码实现的上边的需求:

  1. import kotlinx.coroutines.*
  2. import kotlin.concurrent.thread
  3. @DelicateCoroutinesApi
  4. fun main() {
  5. loop()
  6. coroutineDrink()
  7. coroutineFish()
  8. coroutineWork()
  9. threadDrink()
  10. threadFish()
  11. threadWork()
  12. println("program finish")
  13. }
  14. @DelicateCoroutinesApi
  15. fun coroutineDrink(){
  16. GlobalScope.launch(Dispatchers.IO) {
  17. delay(3000)
  18. println("我要立马喝水 :"+Thread.currentThread().name)
  19. }
  20. }
  21. @DelicateCoroutinesApi
  22. fun coroutineWork(){
  23. GlobalScope.launch(Dispatchers.IO) {
  24. delay(500)
  25. println("我在打代码 :"+Thread.currentThread().name)
  26. }
  27. }
  28. @DelicateCoroutinesApi
  29. fun coroutineFish(){
  30. GlobalScope.launch(Dispatchers.IO) {
  31. delay(1000)
  32. println("我在摸鱼 :"+Thread.currentThread().name)
  33. }
  34. }
  35. @DelicateCoroutinesApi
  36. fun threadDrink(){
  37. thread {
  38. Thread.sleep(3000)
  39. println("我要立马喝水 :"+Thread.currentThread().name)
  40. }
  41. }
  42. @DelicateCoroutinesApi
  43. fun threadWork(){
  44. thread {
  45. Thread.sleep(500)
  46. println("我在打代码 :"+Thread.currentThread().name)
  47. }
  48. }
  49. @DelicateCoroutinesApi
  50. fun threadFish(){
  51. thread {
  52. Thread.sleep(1000)
  53. println("我在摸鱼 :"+Thread.currentThread().name)
  54. }
  55. }

运行这段程序后,输入如下:
image.png
通过上边输出可以看到,在协程里你的名字叫 DefaultDispatcher-worker-1 。是一个线程
而在线程里,通过输出你也能看到,想要在一个线程里实现摸鱼工作输出,是不能的,这里的输出是三个线程

提示:这个代码例子依然粗暴,我不想让你造成片面的印象,这里重点提一下,线程是可以实现挂起的,而且实现起来也不难(当然没有协程简单 : ] ),如果你有兴趣可以考虑实现下,因为这里重点讲协程,我就不多写了

Kotlin 中的挂起

上边通过一个例子大致的了解了下挂起的概念,下面我们在结合 Kotlin 代码,再简单的说下挂起和挂起函数,现在先往前倒一下第一个例子中的 delay 函数,如下图:

image.png

上图中红框表示此处正在调用一个挂起函数,这个统称为挂起点
自定义挂起函数、自定义挂起 Lambda 就更简单了,加上 suspend 关键词就可以

image.png

如何把现有函数封装为挂起函数

之所以有这一段,是因为确实有个实际场景,在我对协程了解不多的情况下,我用过更多是协程的同步功能和线程自动切换功能,但是对于一些不支持协程的库或者老项目,我该咋用呢,例如下面的 LoginModel

  1. class LoginModel {
  2. fun login(callback: Callback) {
  3. thread {
  4. Thread.sleep(2000)
  5. callback.result(true)
  6. }
  7. }
  8. interface Callback {
  9. fun result(boolean: Boolean)
  10. }
  11. }

这个 login 函数是个很典型的通过回调来通知登录结果形式的,那如果我想把这个函数改成 suspend 的该如何改呢,下面的代码可以做个参考

  1. @DelicateCoroutinesApi
  2. fun coroutineTest() {
  3. val model = LoginModel()
  4. GlobalScope.launch {
  5. println("in coroutine " + Thread.currentThread().name)
  6. val loginResult = model.suspendLogin()
  7. println("loginResult is $loginResult")
  8. println("end coroutine " + Thread.currentThread().name)
  9. }
  10. }
  11. suspend fun LoginModel.suspendLogin(): Boolean = suspendCancellableCoroutine {
  12. login(object : LoginModel.Callback {
  13. override fun result(resultData: Boolean) {
  14. println("in result " + Thread.currentThread().name)
  15. it.resume(resultData)
  16. }
  17. })
  18. }

上边的代码中,给 LoginModel 增加了一个挂起扩展函数 suspendLogin,这个挂起函数顶层是被 suspendCancellableCoroutine 封装,通过这个封装,可以拿到编译器给 suspend 函数添加的 Continuation 参数,通过这个参数,就可以实现挂起函数的恢复。有可能你看到这坨解释会一头雾水,这个随着你对协程的理解的深入,会慢慢了解的,知识库都会讲,可以先记住并忽略。

运行下程序,查看下输出:
image.png
可以看到得到结果时的线程和开启协程作用域的线程是一致的,而 result 回调里的线程是单独的线程,证明线程在这里已经自动切换了,符合目标,完事儿。

作用域与上下文


作用域

作用域在上边多少提到了一点点,例如 GlobalScope 这个。
作用域是个接口,源代码很简单,如下:

  1. //CoroutineScope.kt
  2. public interface CoroutineScope {
  3. public val coroutineContext: CoroutineContext
  4. }

作用域是啥我这里说一个实际的开发场景感受下它的作用:

就举 Android 开发的例子吧,我在一个 Activity 页面异步请求了 n 个接口,在接口还没有返回的时候,用户就点击退出这个页面,在页面退出后我并不想再去响应这些返回值,因为有可能会造成内存泄漏、页面异常、信息异常等问题。所以我需要在 Activity 的 onDestory 的回调里取消这 n 个接口的请求,但是如果想要主动的取消请求,那么我肯定得有此次请求的实例,那么 n 个请求就会有 n 个实例(恐怖如斯)。
那有没有方式可以一次把这 n 个实例都取消掉呢,如果你使用的是协程,那么事情就简单了,你可以把这 n 个协程请求都附加在一个作用域上,只要作用域执行取消了,这 n 个请求就随之而同取消。

现在你再感受下 Android Jetpack 框架的 ViewModel 提供的作用域 viewModelScope 的作用 尤其 ViewModel 完全契合 Lifecycle,这一套逻辑就很舒畅

下面是个模拟的代码片段,可以运行感受下

  1. @DelicateCoroutinesApi
  2. fun main() {
  3. loop()
  4. println("---进入 Activity")
  5. val scope = CoroutineScope(Dispatchers.IO)
  6. scope.launch {
  7. delay(1000)
  8. println("请求 1 完成")
  9. }
  10. scope.launch {
  11. delay(1100)
  12. println("请求 2 完成")
  13. }
  14. scope.launch {
  15. delay(1400)
  16. println("请求 n 完成")
  17. }
  18. GlobalScope.launch {
  19. delay(500)
  20. println("---离开 Activity")
  21. scope.cancel()
  22. }
  23. }

刚刚通过一个例子,说了作用域的一个主要功能,在这里你只需要有个大概的了解,深入的话题会在后续的文章中再详细解析。

Tips: cancel 函数是通过扩展函数实现的

那么我们现在再回过头来看看作用域接口的源码,里面就一个 CoroutineContext 类型的参数,那么这个又是什么

协程上下文

CoroutineContext

如果你是 Android 开发人员或者是 Spring 开发人员,那么肯定不会对 Context 字段陌生,在 Android 的 App 里 Context 存储了当前 App 的各种基础信息以及资源和各种顶级操作,我无法笼统的概述 Android 的 Context 是什么,因为它做了太多事情,在协程里也一样,我并没法去概述协程上下文是什么,只能说是遇到哪个学哪个。

协程的上下文的实现起来很精妙,它不像是 Android 的 Context 一样把功能都写死,而是通过类似于组合模式把功能拼装起来,每一个功能都是一个 Element。

你可以把 CoroutineContext 想象成一个功能 Map,需要什么 put 什么,例如下面代码,通过 ‘+’ 号组合了调度器和命名的功能

  1. withContext(Dispatchers.IO + CoroutineName("测试功能 1")) {
  2. println("Dispatchers.IO " + Thread.currentThread().name)
  3. println(coroutineContext[CoroutineName.Key])
  4. }

上下文的功能有不少,但是眼下,基于此篇文章来说我认为最起码需要了解下文的以下两个功能:

  • 协程线程调度器 CoroutineDispatcher
  • 协程任务管理器 Job

协程线程调度器

使用协程最愉悦的地方就是在于它的自动线程切换功能,而这个就是通过 CoroutineDispatcher 实现的。
协程基础库里提供了 Dispatchers 类用实现了 4 种基本的调度器,这个在上边的例子里多少都出现过

  • Dispatchers.Default — 默认的调度器,它使用的是 Executors.newFixedThreadPool 线程池实现的,最大的并发量是设备 CPU 的内核数,最少为 2
  • Dispatchers.IO — 执行阻塞任务的调度器,默认可以并发 64 个线程。由于协程是共享的线程,所以使用 withContext 可能不会导致实际切换线程,通常是在同一个线程中继续执行
  • Dispatchers.Main — 这个调度器被限制在操作 UI 对象的主线程中。通常这样的调度器都是单线程的。为了能使用此调度器,你有可能需要在项目中添加如下依赖:
    • kotlinx-coroutines-android:使用 Android 主线程
    • kotlinx-coroutines-javafx:JavaFx 项目的 Application 线程
    • kotlinx-coroutines-swing:Swing 项目的 EDT 线程
    • kotlinx-coroutines-test:测试项目
  • Dispatchers.Unconfined — 不会限定任何线程的调度器,它不会主动切换线程,而是延续上一个挂起时使用的线程

    1. scope.launch(Dispatchers.Main) {
    2. launch(Dispatchers.IO) {
    3. }
    4. withContext(Dispatchers.Default){
    5. }
    6. }

调度器也是可以自己创建的,Kotlin 为线程池创建了一个 asCoroutineDispatcher 的扩展函数可以方便的创建

  1. @JvmName("from") // this is for a nice Java API, see issue #255
  2. public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher =
  3. ExecutorCoroutineDispatcherImpl(this)
  4. @JvmName("from") // this is for a nice Java API, see issue #255
  5. public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher =
  6. (this as? DispatcherExecutor)?.dispatcher ?: ExecutorCoroutineDispatcherImpl(this)

使用起来也非常简单,下面是个简单的示例

  1. val scope = CoroutineScope(EmptyCoroutineContext)
  2. val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
  3. scope.launch(dispatcher) {
  4. }

协程任务

刚才在作用域那一节里,我们已经知道了作用域是如何取消的,那单个协程是否能取消呢

Job

Job 是启动协程后的一个实例对象,对协程有这影响生命周期的操作,例如上边所说的取消函数

  1. val scope = CoroutineScope(EmptyCoroutineContext)
  2. val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
  3. val job = scope.launch(dispatcher){
  4. }
  5. job.cancel()

关于取消函数,这里还可以再多说一点,它是具有父子连带关系的,父 Job 如果取消了其子 Job 也会随之取消,例如下面的例子:

  1. val scope = CoroutineScope(EmptyCoroutineContext)
  2. val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
  3. val job = scope.launch(dispatcher) {
  4. val childJob = launch {
  5. delay(1000)
  6. println("childJob end")
  7. }
  8. val notChildJob = scope.launch {
  9. delay(1000)
  10. println("notChildJob end")
  11. }
  12. }
  13. job.cancel()

childJob end 就不会输出,而 notChildJob end 会输出

Job 跟 Thread 的一样,都是有状态的,Thread 的状态如果不了解的话,可以参考下这篇文章
线程(也是我写的 : ] ),Job 的状态我在这里不会过多说什么,但是对于协程的理解和灵活运用还是挺有必要的,所以以后有可能会单独抽出一篇文章来讲解一下。而它的实际应用场景还是有的,例如我想先定义一个协程任务,但是不启动,等我需要的时候在运行启动:

  1. val scope = CoroutineScope(EmptyCoroutineContext)
  2. val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
  3. val job = scope.launch(context = dispatcher, start = CoroutineStart.LAZY) {
  4. println("开始执行 Job ")
  5. delay(2000)
  6. println("执行完毕 job")
  7. }
  8. scope.launch {
  9. println(job.key)
  10. delay(2000)
  11. println("2s 过去了,启动 job")
  12. job.start()
  13. }

CompletableJob

上边说了调用 launch 函数会返回 Job 对象,但是有个 Job 函数有可能会对你产生混淆和误导,千万不要认为 Job 函数 是 Job 接口的构造函数,不是的,Job 函数返回的是 CompletableJob 对象
这里简单说下这个是啥 val job1 = Job(),我们还是先看下 Job 函数的源码

  1. @Suppress("FunctionName")
  2. public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)

发现其返回的是一个 CompletableJob 对象,关于 CompletableJob 的用法,这里拿个实际需求场景去感受下

就拿个人中心页面来说,假设这个页面有两个接口,一个用来获取用户头像、一个用来获取用户电话。需求呢是想要这两个信息都有了后,再显示页面,否则页面一直是加载状态
作为开发的角度,这两个接口没有逻辑联系所以同步请求是最优解,但是怎么去保证两个接口都有数据了后再执行后续逻辑呢

上边这个需求就可以通过 CompletableJob 实现,话不多说了代码如下:

  1. scope.launch {
  2. val headJob = Job()
  3. val phoneJob = Job()
  4. var headData:String? = null
  5. var phoneData:String? = null
  6. launch {
  7. delay(1000)
  8. headJob.complete()
  9. println("头像请求完成")
  10. headData = "我是头像数据"
  11. }
  12. launch {
  13. delay(1500)
  14. phoneJob.complete()
  15. println("电话请求完成")
  16. phoneData = "我是电话数据"
  17. }
  18. joinAll(headJob, phoneJob)
  19. val endTime = System.currentTimeMillis()
  20. println(headData)
  21. println(phoneData)
  22. println("执行完成 请求时间:${endTime - startTime}")
  23. }

Deferred

Deferred 是 Job 的一个实现,在 CompletableJob 中的需求,我们同样可以通过 Deferred 实现,代码如下:

  1. scope.launch {
  2. val headJob = async(start = CoroutineStart.LAZY) {
  3. delay(1000)
  4. println("头像请求完成")
  5. "我是头像数据"
  6. }
  7. val phoneJob = async(start = CoroutineStart.LAZY) {
  8. delay(1500)
  9. println("电话请求完成")
  10. "我是电话数据"
  11. }
  12. val headData = headJob.await()
  13. val phoneData = phoneJob.await()
  14. val endTime = System.currentTimeMillis()
  15. println(headData)
  16. println(phoneData)
  17. println("执行完成 请求时间:${endTime - startTime}")
  18. }

CompletableJob 和 Deferred 这两个都能实现高效并发,但是从主观上来讲,Deferred 是种更符合思维逻辑的实现

�结语


这篇文章里的例子,最好都去运行下,感受下,实际的收获是要比只看要多的。

至此协程的入门我觉得是应该够了,但是还有很多实用的功能和实现并没有说到,例如拦截器和 Flow 等。这些呢并不属于入门的范畴,我会单独的去开文章去写。

其实入门已经能满足大部分开发需求了,但是如果你在寻求一些功能的更加优雅的实现,那么我还是建议这个系列要看完。