前言
在最开始 协程初探 的文章中,我们已经把协程的基本概念给出了定义:
kotlin 协程跟Java线程概念类似,都是用来管理并发的工具。它的底层是使用Java线程实现的,并基于线程封装了一套 API
给我们使用,让我们能够更容易的管理并发任务。
如果有人问你什么是协程,我们大概率会从概念的角度来回答,但是,概念往往都是比较抽象的,不是那么容易理解。
我们在开发中,往往是通过实际的代码来理解和使用协程的,下面,我们通过代码的角度来理解一下什么是协程。
代码中的线程
我们使用协程基本上都是为了替代掉线程,因此,往往在学习协程的时候都会拿线程来做对比。
所以在说协程之前,我们先来看看线程,在代码中,线程的具像化表现就是Thread类,我们通过Thread类来创建线程对象,然后使用线程对象来执行任务。
kotlin
//线程对象
val thread = Thread {
//执行任务
}
//线程的操作
thread.start()//启动线程
thread.interrupt()//中断线程
thread.join()//等待线程结束
//线程的状态和属性
thread.priority//线程优先级
thread.name//线程名字
thread.id//线程id
thread.isAlive//线程是否存活
thread.isDaemon//线程是否是守护线程
thread.isInterrupted//线程是否被中断
thread.state//线程状态
从上述代码可以看出,线程在代码中的具像化表现是比较直观的,我们通过Thread类直接创建线程对象,然后通过线程的API来操作线程以及获取线程的属性和状态。
代码中的协程
Kotlin协程并没有给我们提供一个类似Thread的类来直接创建协程对象,而是通过 CoroutineScope
的扩展方法例如 launch
,async
等方式来创建新的协程。
代码示例:
kotlin
//协程的创建
GlobalScope.launch {
//执行任务
}
源码分析
以 launch
的源码为例:
launch
函数中会根据传入的 CoroutineStart
参数来创建一个协程对象 coroutine
,然后调用 coroutine.start
来启动协程。最后把 coroutine
返回。
需要注意的是返回值的类型是 Job
,而不是 LazyStandaloneCoroutine
或者 StandaloneCoroutine
类型。
这点跟线程不同,线程非常直观,返回值的类型就是 Thread
,kotlin给我们提供的创建协程的api的返回值类型却是 Job
这里我们先思考一下,为什么要把返回值类型限定为 Job
?
接着看 launch
的源码,其中, LazyStandaloneCoroutine
是私有的,继承自 StandaloneCoroutine
StandaloneCoroutine
也是私有的,继承自 AbstractCoroutine
再来看看 AbstractCoroutine
的源码。
AbstractCoroutine
是一个抽象类,它继承或实现了以下几个接口或类:
接口/类 | 主要功能 |
---|---|
Job (接口) | 定义了协程的基本操作和状态查询方法。它是协程的最基本的生命周期管理接口。 |
JobSupport (类) | JobSupport 是抽象类,实现了 Job 接口,提供了包括状态管理、取消机制、完成回调等具体的实现。 |
Continuation(接口) | 管理协程的挂起和恢复执行,并携带协程的上下文。 |
CoroutineScope (接口) | 协程作用域,提供了协程的创建和管理方法。 |
这也是为什么 launch
函数的返回值类型可以是 Job
,因为 AbstractCoroutine
实现了 Job
接口。
Job
Job是一个接口,它定义了协程的基本操作和状态查询方法。其中很多api跟线程比较相近。
代码示例:
kotlin
//创建一个协程,其返回值是Job类型
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
println("Coroutine: ${Thread.currentThread().name}")
}
//协程的操作
job.start()//启动协程
job.cancel()//取消协程
job.join()//等待协程结束
job.invokeOnCompletion {}//协程结束时的回调
job.cancelAndJoin()//取消并等待协程结束
job.cancelChildren()//取消协程的子协程
//协程的状态和属性
job.isActive//协程是否是活动状态
job.isCancelled//协程是否被取消
job.isCompleted//协程是否完成
job.parent//父协程
job.children//子协程
上面也提到了,launch
函数的返回值类型被限定为 Job
,这样,我们就只能使用 Job
接口中定义的API来操作协程了。
而 Job
接口主要提供的都是跟流程和状态相关的API。
这个设计细想一下也是合理的,因为我们创建协程的目的是为了执行任务,拿到协程对象后,我们只需要关心协程的执行流程和状态即可。
JobSupport
至于JobSupport,是一个抽象类,实现了Job接口,提供了包括状态管理、取消机制、完成回调等具体的实现。
Continuation
Continuation
我们比较熟悉了,在之前的文章中也有提到过。 本质上就是一个接口,内部持有了一个 CoroutineContext
类型的变量以及提供了一个抽象的 resumeWith
方法。它也是挂起和恢复的桥梁。
CoroutineScope
CoroutineScope
是一个是非常重要的接口,它提供了协程的作用域,官方定义了一些扩展方法用于创建协程,例如 launch
,async
等。
CoroutineScope.launch
CoroutineScope.async
还是以 launch
的源码为例:
该函数的返回值在上面说过了,本质上就是把创建的 coroutine
限定为 Job
类型返回。
我们这里重点来关注下调用 launch
时传入的 block
参数。
block: suspend CoroutineScope.() -> Unit
CoroutineScope.()
表示这是一个接收者为CoroutineScope
类型的对象的扩展函数。也就是说在 block
代码块中会有一个this
指向CoroutineScope
类型的对象,那么在代码块中可以直接访问到CoroutineScope
的属性和方法。- 该扩展被
suspend
修饰,表示它还是一个挂起函数,可以在协程中被调用。 - 该函数的返回值是
Unit
,表示这个函数没有返回值。
也就是说,block
首先是一个函数,然后它还是 CoroutineScope 的扩展函数,最后还是一个挂起函数。
再接着看源码:
在内部创建好 coroutine
对象后,调用 coroutine.start
来启动协程。
这里注意下 start
函数中传入的 coroutine
和 block
参数,我们需要重点关注下类型。
如图标记所示,在 start
函数中,分别对应的形参就是 receiver
和 block
来看下 start
函数的源码:
重点关注 receiver: R 和 block: suspend R.() -> T
中的泛型 R 。
如图标记所示:
在前面我们能够得知, block 本质上就是 CoroutineScope
类型的扩展函数,因此,在这里的泛型 R
就对应 CoroutineScope
类型。
那么,这里的 receiver
的类型就也是 CoroutineScope
类型了。
前面我们分析过,coroutine
最终是作为 launch
的返回值返回的,而且在返回的时候类型被限制成了 Job
。
这一点也比较好理解,因为 AbstractCoroutine
既实现了 Job
接口还实现了 CoroutineScope
接口,因此,coroutine
既可以作为 Job
类型,也可以作为 CoroutineScope
类型。
下面我们来分别打印下 coroutine
和 block
中的 this
kotlin
suspend fun main(args: Array<String>) {
val job = GlobalScope.launch {
println("block代码块中的隐式接收者this: $this")
}
println("job: $job")
}
结果如下:
到这里我们会发现,实际上 coroutine
和 block
中的隐式 CoroutineScope
类型的 this
本质上就是同一个对象
,只是在不同的上下文中被调用。
总结
根据上面的分析,再来看看下面的代码示例,可以思考输出的结果。
kotlin
suspend fun main(args: Array<String>) {
var launchScope: CoroutineScope? = null
//Deferred 继承自Job,本质上就是一个带有结果的 Job
var deffered: Deferred<String>? = null
var asyncScope: CoroutineScope? = null
val job = GlobalScope.launch {
launchScope = this
withContext(Dispatchers.IO) {
delay(1000)
println("withContext this:${this}")
"返回结果"
}
deffered = this.async {
asyncScope = this
"返回结果"
}
}
deffered?.await()
job.join()
println("job: $job")
println("launchScope:${launchScope}")
println("job===launchScope:${job === launchScope}")
println("deffered: $deffered")
println("asyncScope:${asyncScope}")
println("deffered===asyncScope:${deffered === asyncScope}")
}
运行结果:
到这里我们可以尝试从代码的角度来回答 什么是协程? 这个问题了:
协程的具像化理解
我们可以从三个视角来理解协程:
- 通过
CoroutineScope
的扩展方法例如launch
,async
等方式创建出来的协程,会有一个Job
类型的返回值,这个返回的Job
对象就是协程。 - 我们在调用
launch
或者async
方法时传递的block
代码块中隐式的this
,实际上就是CoroutineScope
类型的对象,它是协程。 block
代码块,本质上就是一个CoroutineScope
类型的扩展函数,它也是协程。
Job
和 CoroutineScope
都是协程的具像化表现,只是他们的能力和职责不同。
Job
主要提供了跟流程和状态相关的API,例如启动、取消、等待、状态查询等,提供给开发者操作协程的能力。CoroutineScope
则范围更广,除了操作协程外,还提供了创建协程的方法,例如launch
,async
等,我们在开发中主要也都是使用CoroutineScope
创建协程的能力。
这样做也是为了把职责分离,让开发者能够更自然地使用协程的API。
本篇文章算是在初步会使用协程后对概念的加深,只有概念理解清楚了,后面再去理解更高级的特性才能事半功倍。
好了,本篇文章就是这样,希望能帮到你。
感谢阅读,如果对你有帮助请点赞支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客