首先我们先创建一个新的项目,如果不知道怎么创建,先参考 学习基础环境准备 这篇文章。
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.*
@DelicateCoroutinesApi
fun main() {
coroutineTest()
println("program finish")
}
@DelicateCoroutinesApi
fun 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
@DelicateCoroutinesApi
fun main() {
loop()
coroutineDrink()
coroutineFish()
coroutineWork()
threadDrink()
threadFish()
threadWork()
println("program finish")
}
@DelicateCoroutinesApi
fun coroutineDrink(){
GlobalScope.launch(Dispatchers.IO) {
delay(3000)
println("我要立马喝水 :"+Thread.currentThread().name)
}
}
@DelicateCoroutinesApi
fun coroutineWork(){
GlobalScope.launch(Dispatchers.IO) {
delay(500)
println("我在打代码 :"+Thread.currentThread().name)
}
}
@DelicateCoroutinesApi
fun coroutineFish(){
GlobalScope.launch(Dispatchers.IO) {
delay(1000)
println("我在摸鱼 :"+Thread.currentThread().name)
}
}
@DelicateCoroutinesApi
fun threadDrink(){
thread {
Thread.sleep(3000)
println("我要立马喝水 :"+Thread.currentThread().name)
}
}
@DelicateCoroutinesApi
fun threadWork(){
thread {
Thread.sleep(500)
println("我在打代码 :"+Thread.currentThread().name)
}
}
@DelicateCoroutinesApi
fun 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 的该如何改呢,下面的代码可以做个参考
@DelicateCoroutinesApi
fun 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.kt
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
作用域是啥我这里说一个实际的开发场景感受下它的作用:
就举 Android 开发的例子吧,我在一个 Activity 页面异步请求了 n 个接口,在接口还没有返回的时候,用户就点击退出这个页面,在页面退出后我并不想再去响应这些返回值,因为有可能会造成内存泄漏、页面异常、信息异常等问题。所以我需要在 Activity 的 onDestory 的回调里取消这 n 个接口的请求,但是如果想要主动的取消请求,那么我肯定得有此次请求的实例,那么 n 个请求就会有 n 个实例(恐怖如斯)。
那有没有方式可以一次把这 n 个实例都取消掉呢,如果你使用的是协程,那么事情就简单了,你可以把这 n 个协程请求都附加在一个作用域上,只要作用域执行取消了,这 n 个请求就随之而同取消。
现在你再感受下 Android Jetpack 框架的 ViewModel 提供的作用域 viewModelScope 的作用 尤其 ViewModel 完全契合 Lifecycle,这一套逻辑就很舒畅
下面是个模拟的代码片段,可以运行感受下
@DelicateCoroutinesApi
fun 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 #255
public fun ExecutorService.asCoroutineDispatcher(): ExecutorCoroutineDispatcher =
ExecutorCoroutineDispatcherImpl(this)
@JvmName("from") // this is for a nice Java API, see issue #255
public 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? = null
var phoneData:String? = null
launch {
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 等。这些呢并不属于入门的范畴,我会单独的去开文章去写。
其实入门已经能满足大部分开发需求了,但是如果你在寻求一些功能的更加优雅的实现,那么我还是建议这个系列要看完。