首先我们先创建一个新的项目,如果不知道怎么创建,先参考 学习基础环境准备 这篇文章。
Kotlin 本身是带有基础的协程功能的,但是这篇里会用到一些协程的扩展函数,所以需要加上扩展库,添加后的 build.gradle 文件如下:
plugins {id 'org.jetbrains.kotlin.jvm' version '1.5.30'id 'java'}group 'org.example'version '1.0-SNAPSHOT'repositories {mavenCentral()mavenCentral()}dependencies {implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.5.21'implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2-native-mt'}test {useJUnitPlatform()}
协程是什么
在 Kotlin For Jvm 中协程是依附于线程的,线程是依附于进程的,如下图:
线程是系统级别,由系统调度
协程是程序级别,由程序实现创建和调度
所以可以概括协程是一个更轻量级的(相比于线程)异步框架
上边的概念,因具体实现的语言而定,最起码在定义上概念是没有多大出入的
HelloWord
创建项目后,我们先写个协程项目,然后运行下:
import kotlinx.coroutines.*@DelicateCoroutinesApifun main() {coroutineTest()println("program finish")}@DelicateCoroutinesApifun coroutineTest() {GlobalScope.launch {println("in coroutine")}}fun threadTest(){thread(name = "testThread") {println("in thread")}}
你有可能想问 GlobalScope.launch{ … } 是什么,这个可以先忽略,可以暂时理解为 thread{ … }
运行后,查看输出,输出了一行 program finish….恩,恩?不对阿,没有输出 in coroutine ,协程创建失败了,还是我代码有问题?把 coroutineTest 换成 threadTest 试试,额..正确输出了
所以这是为什么??
其实从这个小例子里就能看出来协程和线程从根本上是不同的,至于为什么的话,这里先把问题抛出,毕竟这篇是入门基础,希望你在深入的剖析了协程后,可以还记得这个问题,然后运用你所学的知识武器攻破它。
那么转过头来,眼下这个问题怎么解决呢,我先剧透点信息因为你的主程序先于协程结束了,所以协程体内的代码不会执行。那么只要想办法让主程序不结束不就好了,这个方式有很多,这里我随便写一个 loop 函数,你也可以用自己的方式实现,例如更加 Kotlin 的写法 runBlocking { … }
fun loop() {thread {while (true) {Thread.sleep(1000)}}}
现在我们把 loop 函数放在 main 函数的第一行,然后再次运行
终于成功输出了,那么你的第一个协程程序就完成了。
挂起
挂起跟阻塞有相似之处,但是却是完全不同
你问什么是阻塞?Thread.sleep(..)、网络请求啥的阻塞当前线程运行的,都叫阻塞
挂起与阻塞
在网上查询挂起和阻塞的相关概念能查到一堆专业的解答,而我才学疏浅一个没看懂… …
其实没懂也不至于,只是不知道怎么归纳的简单容易理解,所以这里我只能用现实中的例子来说明下阻塞和挂起的区别,例如下面的场景:
现在时间 10:30,你想要在 11:00 的时候喝水,这时你把你自己比作一个线程
- 阻塞:10:30~11:00 之间,你啥也不干,就等着到点,然后立马喝水
- 挂起:你定了个 11:00 的闹钟,在10:30~11:00 之间你有可能打代码,有可能摸鱼,有可能巴拉巴拉…等闹钟一响,立马喝水
下面使用代码实现的上边的需求:
import kotlinx.coroutines.*import kotlin.concurrent.thread@DelicateCoroutinesApifun main() {loop()coroutineDrink()coroutineFish()coroutineWork()threadDrink()threadFish()threadWork()println("program finish")}@DelicateCoroutinesApifun coroutineDrink(){GlobalScope.launch(Dispatchers.IO) {delay(3000)println("我要立马喝水 :"+Thread.currentThread().name)}}@DelicateCoroutinesApifun coroutineWork(){GlobalScope.launch(Dispatchers.IO) {delay(500)println("我在打代码 :"+Thread.currentThread().name)}}@DelicateCoroutinesApifun coroutineFish(){GlobalScope.launch(Dispatchers.IO) {delay(1000)println("我在摸鱼 :"+Thread.currentThread().name)}}@DelicateCoroutinesApifun threadDrink(){thread {Thread.sleep(3000)println("我要立马喝水 :"+Thread.currentThread().name)}}@DelicateCoroutinesApifun threadWork(){thread {Thread.sleep(500)println("我在打代码 :"+Thread.currentThread().name)}}@DelicateCoroutinesApifun threadFish(){thread {Thread.sleep(1000)println("我在摸鱼 :"+Thread.currentThread().name)}}
运行这段程序后,输入如下:
通过上边输出可以看到,在协程里你的名字叫 DefaultDispatcher-worker-1 。是一个线程
而在线程里,通过输出你也能看到,想要在一个线程里实现摸鱼工作输出,是不能的,这里的输出是三个线程
提示:这个代码例子依然粗暴,我不想让你造成片面的印象,这里重点提一下,线程是可以实现挂起的,而且实现起来也不难(当然没有协程简单 : ] ),如果你有兴趣可以考虑实现下,因为这里重点讲协程,我就不多写了
Kotlin 中的挂起
上边通过一个例子大致的了解了下挂起的概念,下面我们在结合 Kotlin 代码,再简单的说下挂起和挂起函数,现在先往前倒一下第一个例子中的 delay 函数,如下图:

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

如何把现有函数封装为挂起函数
之所以有这一段,是因为确实有个实际场景,在我对协程了解不多的情况下,我用过更多是协程的同步功能和线程自动切换功能,但是对于一些不支持协程的库或者老项目,我该咋用呢,例如下面的 LoginModel
class LoginModel {fun login(callback: Callback) {thread {Thread.sleep(2000)callback.result(true)}}interface Callback {fun result(boolean: Boolean)}}
这个 login 函数是个很典型的通过回调来通知登录结果形式的,那如果我想把这个函数改成 suspend 的该如何改呢,下面的代码可以做个参考
@DelicateCoroutinesApifun coroutineTest() {val model = LoginModel()GlobalScope.launch {println("in coroutine " + Thread.currentThread().name)val loginResult = model.suspendLogin()println("loginResult is $loginResult")println("end coroutine " + Thread.currentThread().name)}}suspend fun LoginModel.suspendLogin(): Boolean = suspendCancellableCoroutine {login(object : LoginModel.Callback {override fun result(resultData: Boolean) {println("in result " + Thread.currentThread().name)it.resume(resultData)}})}
上边的代码中,给 LoginModel 增加了一个挂起扩展函数 suspendLogin,这个挂起函数顶层是被 suspendCancellableCoroutine 封装,通过这个封装,可以拿到编译器给 suspend 函数添加的 Continuation 参数,通过这个参数,就可以实现挂起函数的恢复。有可能你看到这坨解释会一头雾水,这个随着你对协程的理解的深入,会慢慢了解的,知识库都会讲,可以先记住并忽略。
运行下程序,查看下输出:
可以看到得到结果时的线程和开启协程作用域的线程是一致的,而 result 回调里的线程是单独的线程,证明线程在这里已经自动切换了,符合目标,完事儿。
作用域与上下文
作用域
作用域在上边多少提到了一点点,例如 GlobalScope 这个。
作用域是个接口,源代码很简单,如下:
//CoroutineScope.ktpublic interface CoroutineScope {public val coroutineContext: CoroutineContext}
作用域是啥我这里说一个实际的开发场景感受下它的作用:
就举 Android 开发的例子吧,我在一个 Activity 页面异步请求了 n 个接口,在接口还没有返回的时候,用户就点击退出这个页面,在页面退出后我并不想再去响应这些返回值,因为有可能会造成内存泄漏、页面异常、信息异常等问题。所以我需要在 Activity 的 onDestory 的回调里取消这 n 个接口的请求,但是如果想要主动的取消请求,那么我肯定得有此次请求的实例,那么 n 个请求就会有 n 个实例(恐怖如斯)。
那有没有方式可以一次把这 n 个实例都取消掉呢,如果你使用的是协程,那么事情就简单了,你可以把这 n 个协程请求都附加在一个作用域上,只要作用域执行取消了,这 n 个请求就随之而同取消。
现在你再感受下 Android Jetpack 框架的 ViewModel 提供的作用域 viewModelScope 的作用 尤其 ViewModel 完全契合 Lifecycle,这一套逻辑就很舒畅
下面是个模拟的代码片段,可以运行感受下
@DelicateCoroutinesApifun main() {loop()println("---进入 Activity")val scope = CoroutineScope(Dispatchers.IO)scope.launch {delay(1000)println("请求 1 完成")}scope.launch {delay(1100)println("请求 2 完成")}scope.launch {delay(1400)println("请求 n 完成")}GlobalScope.launch {delay(500)println("---离开 Activity")scope.cancel()}}
刚刚通过一个例子,说了作用域的一个主要功能,在这里你只需要有个大概的了解,深入的话题会在后续的文章中再详细解析。
Tips: cancel 函数是通过扩展函数实现的
那么我们现在再回过头来看看作用域接口的源码,里面就一个 CoroutineContext 类型的参数,那么这个又是什么
协程上下文
CoroutineContext
如果你是 Android 开发人员或者是 Spring 开发人员,那么肯定不会对 Context 字段陌生,在 Android 的 App 里 Context 存储了当前 App 的各种基础信息以及资源和各种顶级操作,我无法笼统的概述 Android 的 Context 是什么,因为它做了太多事情,在协程里也一样,我并没法去概述协程上下文是什么,只能说是遇到哪个学哪个。
协程的上下文的实现起来很精妙,它不像是 Android 的 Context 一样把功能都写死,而是通过类似于组合模式把功能拼装起来,每一个功能都是一个 Element。
你可以把 CoroutineContext 想象成一个功能 Map,需要什么 put 什么,例如下面代码,通过 ‘+’ 号组合了调度器和命名的功能
withContext(Dispatchers.IO + CoroutineName("测试功能 1")) {println("Dispatchers.IO " + Thread.currentThread().name)println(coroutineContext[CoroutineName.Key])}
上下文的功能有不少,但是眼下,基于此篇文章来说我认为最起码需要了解下文的以下两个功能:
- 协程线程调度器 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 — 不会限定任何线程的调度器,它不会主动切换线程,而是延续上一个挂起时使用的线程
scope.launch(Dispatchers.Main) {launch(Dispatchers.IO) {}withContext(Dispatchers.Default){}}
调度器也是可以自己创建的,Kotlin 为线程池创建了一个 asCoroutineDispatcher 的扩展函数可以方便的创建
@JvmName("from") // this is for a nice Java API, see issue #255public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher =ExecutorCoroutineDispatcherImpl(this)@JvmName("from") // this is for a nice Java API, see issue #255public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher =(this as? DispatcherExecutor)?.dispatcher ?: ExecutorCoroutineDispatcherImpl(this)
使用起来也非常简单,下面是个简单的示例
val scope = CoroutineScope(EmptyCoroutineContext)val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()scope.launch(dispatcher) {}
协程任务
刚才在作用域那一节里,我们已经知道了作用域是如何取消的,那单个协程是否能取消呢
Job
Job 是启动协程后的一个实例对象,对协程有这影响生命周期的操作,例如上边所说的取消函数
val scope = CoroutineScope(EmptyCoroutineContext)val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()val job = scope.launch(dispatcher){}job.cancel()
关于取消函数,这里还可以再多说一点,它是具有父子连带关系的,父 Job 如果取消了其子 Job 也会随之取消,例如下面的例子:
val scope = CoroutineScope(EmptyCoroutineContext)val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()val job = scope.launch(dispatcher) {val childJob = launch {delay(1000)println("childJob end")}val notChildJob = scope.launch {delay(1000)println("notChildJob end")}}job.cancel()
childJob end 就不会输出,而 notChildJob end 会输出
Job 跟 Thread 的一样,都是有状态的,Thread 的状态如果不了解的话,可以参考下这篇文章
线程(也是我写的 : ] ),Job 的状态我在这里不会过多说什么,但是对于协程的理解和灵活运用还是挺有必要的,所以以后有可能会单独抽出一篇文章来讲解一下。而它的实际应用场景还是有的,例如我想先定义一个协程任务,但是不启动,等我需要的时候在运行启动:
val scope = CoroutineScope(EmptyCoroutineContext)val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()val job = scope.launch(context = dispatcher, start = CoroutineStart.LAZY) {println("开始执行 Job ")delay(2000)println("执行完毕 job")}scope.launch {println(job.key)delay(2000)println("2s 过去了,启动 job")job.start()}
CompletableJob
上边说了调用 launch 函数会返回 Job 对象,但是有个 Job 函数有可能会对你产生混淆和误导,千万不要认为 Job 函数 是 Job 接口的构造函数,不是的,Job 函数返回的是 CompletableJob 对象
这里简单说下这个是啥 val job1 = Job(),我们还是先看下 Job 函数的源码
@Suppress("FunctionName")public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
发现其返回的是一个 CompletableJob 对象,关于 CompletableJob 的用法,这里拿个实际需求场景去感受下
就拿个人中心页面来说,假设这个页面有两个接口,一个用来获取用户头像、一个用来获取用户电话。需求呢是想要这两个信息都有了后,再显示页面,否则页面一直是加载状态
作为开发的角度,这两个接口没有逻辑联系所以同步请求是最优解,但是怎么去保证两个接口都有数据了后再执行后续逻辑呢
上边这个需求就可以通过 CompletableJob 实现,话不多说了代码如下:
scope.launch {val headJob = Job()val phoneJob = Job()var headData:String? = nullvar phoneData:String? = nulllaunch {delay(1000)headJob.complete()println("头像请求完成")headData = "我是头像数据"}launch {delay(1500)phoneJob.complete()println("电话请求完成")phoneData = "我是电话数据"}joinAll(headJob, phoneJob)val endTime = System.currentTimeMillis()println(headData)println(phoneData)println("执行完成 请求时间:${endTime - startTime}")}
Deferred
Deferred 是 Job 的一个实现,在 CompletableJob 中的需求,我们同样可以通过 Deferred 实现,代码如下:
scope.launch {val headJob = async(start = CoroutineStart.LAZY) {delay(1000)println("头像请求完成")"我是头像数据"}val phoneJob = async(start = CoroutineStart.LAZY) {delay(1500)println("电话请求完成")"我是电话数据"}val headData = headJob.await()val phoneData = phoneJob.await()val endTime = System.currentTimeMillis()println(headData)println(phoneData)println("执行完成 请求时间:${endTime - startTime}")}
CompletableJob 和 Deferred 这两个都能实现高效并发,但是从主观上来讲,Deferred 是种更符合思维逻辑的实现
�结语
这篇文章里的例子,最好都去运行下,感受下,实际的收获是要比只看要多的。
至此协程的入门我觉得是应该够了,但是还有很多实用的功能和实现并没有说到,例如拦截器和 Flow 等。这些呢并不属于入门的范畴,我会单独的去开文章去写。
其实入门已经能满足大部分开发需求了,但是如果你在寻求一些功能的更加优雅的实现,那么我还是建议这个系列要看完。
