本文是关于kotlin
协程基础内容的学习总结,其中可能存在认知错误或者不够深入的地方,欢迎大佬们吐槽...
本篇文章包括如下内容:
接下来应该还有协程如下资料:
2020年4月20日 发布第一版
协程和线程从中文发音来将,都是很类似的,但是他们是不同的。
Java中的线程是和系统的线程一一对应的,Java应用中的一个线程就是系统的一个线程。
协程是运行在Java线程中的更加轻量级的调度单元
协程的由来是因为线程的如下几个问题:
线程和协程功能很类似,在不让父线程停止工作的前提下,继续进行某项其他工作
join
调用sleep
或者wait
等函数,会让当前的线程阻塞,阻塞期间,这个线程就无法做其他事情了,白白浪费。协程
在调用suspend
方法的时候,不会阻塞,而是挂起,执行该协程的线程是可以继续进行其他操作的,从资源利用率来说,是更加的高校的。协程比线程更加可控,协程可以很简单的指定启动模式
和挂起恢复后的上下文
。
协程可以让应用只是在等待响应而不是阻塞。
总结下,其特点:
我们从一个典型的协程扩展方法launch
来查看下这3大参数
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
如上所述分别是
重点:协程被挂起后,当需要恢复的时候,是可以在指定的线程恢复的,那么协程上下文(协程分发器)起到的作用就是定义这个恢复的线程。
在开发
Android
或者Swing
的时候,用的特别多,因为渲染数据需要在UI
线程操作。
在不同的平台下,有不同的内置的协程上下文,这里我主要描述Android
和JVM 后端
Java
平台是ForkJoinPool
,相比于IO
更适合执行CPU
密集型的任务。IO
操作。Android
平台,Java
后端代码不可用,当需要更新UI
操作的时候,需要该上下文。除了这些内置的上下文,我们也可以定义自己需要的上下文。
asCoroutineDispatcher()
,你需要注意的是关闭这个线程池// 简单使用
val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
//正规使用
Executors.newSingleThreadExecutor().asCoroutineDispatcher().use {
withContext(it){
delay(1000)
log("hello...")
}
}
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun destroy() {
cancel() // Extension on CoroutineScope
}
// to be continued ...
// class Activity continues
fun doSomething() {
// launch ten coroutines for a demo, each working for a different time
repeat(10) { i ->
launch {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
}
}
} // class Activity ends
一个协程的代码定义好后,如何启动是可以有不同的定义的:
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
我们接下来就来看看这几个不同的区别
DEFAULT 是默认的 启动模式,父线程中的定义好协程后会马上启动,同时不会阻塞父线程的原有执行逻辑。说人话就是:父线程和子协程一同执行。
ATOMIC 在执行逻辑上,同DEFAULT,都是父线程和子协程一同执行,不过不同点在于:DEFAULT可以在子协程还没有进行第一次启动的时候就取消,AUTOMIC却不可以。
请注意:协程在执行了第一次后,不管启动模式是DEFAULT还是ATOMIC都是可以取消的。
LAZY 如同字面意思一样,懒
:协程定义好后,不会马上执行,而是在调用其join
或者await
后才开始执行。
UNDISPACHTERD 是比较特殊的启动模式,通过该参数启动的协程,会将父线程的运行权抢夺过来,所以此时的父线程是阻塞着的,直到协程被挂起,父线程才能继续往下运行。
其中只有DEFAULT和LAZY 是稳定的,其余2者都是
实验性
的API.
从JVM的服务器应用场景来看,目前只用到了DEFAULT
。
就是核心的执行代码。
Deferred
,可以通过await
获取结果上面我们提到了协程的3大参数,那么我们在使用协程构建器的时候,必须清晰的认识到,使用如下这些协程构建器的时候,这3大参数分别是什么。后面分析!
通常会用到线程和协程的入口处,起到的承上启下作用。他的作用是阻塞线程的执行,直到runBlocking
的内部协程
执行完毕为止。
这里需要强调一点的是:通过Globalscope.xxx
的可不是内部协程
。
fun main()= runBlocking {
launch {
delay(2000)
println("...2...")
}
println("...1...")
}
上述结果为如下所示,此时的launch{}
没有指定上下文,默认用的是 runBlocking
的上下文哦,不是默认的DEAULT
。
...1...
...2...
Process finished with exit code 0
但是如果我将launch()
修改为GlobalScope.luanch{}
:
fun main()= runBlocking{
//这里加了 GlobalScope
GlobalScope.launch {
delay(2000)
println("...2...")
}
println("...1...")
}
那么结果如下可以看出:没有输出2。
原因如上所述,通过GlobalScope
启动的协程已经不是runBlocking
里面的协程了,而是顶级协程了,所以runBlocking 不会等待这个顶层的协程执行结束。
...1...
Process finished with exit code 0
这一点希望大家在使用的时候,特别注意了。
launch
启动的是一个不带返回值的协程,返回值为Job
;
async
刚好相反:返回的是一个带有返回值的表示未来
会有结果的一个类型:Deferred<T>
。
Job
和Deferred
的关系如下面源码所示:
public interface Deferred<out T> : Job {
public suspend fun await(): T
}
await
来获取结果我们这里来看下一个async
的用法示例:
todo
既然协程有上下文
和启动模式
,那么上述的runBlocking
和launch
、Async
在不同情况下,对应的上下文分别是什么了?
我们这里可以分为2类来看看这 3 者源码的签名是什么样的!
第一类:async 和 launch
//
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {}
//
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {}
第二类:runBlocking
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
不管是我分的第一类还是第二类,它们对于context
这 3 者默认的上下文都是EmptyCoroutineContext
,那么这个空的上下文
是一个什么东西了?
在第一类,也就是async
和launch
中,源码注释中给出的EmptyContext
说明:
* The coroutine context is inherited from a [CoroutineScope]. Additional context elements can be specified with [context] argument.
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.
简单翻译如下:
* 协程的上下文从[CoroutineScope]继承。你也可以通过[context]来指定特定的上下文。
* 当上下文中没有任何分发器或者[ContinuationInterceptor]的时候,上下文就是[Dispatchers.Default]
第一类async
和launch
的上下文规则如下:
Dispatchers.Default
第二类runBlocking
不一样,其上下文规则是这样的:
我们通过代码来深化理解:
import kotlinx.coroutines.*
import java.util.concurrent.Executors
fun main() {
//没有指定,也没有协程上下文可以继承,那么就是 Dispatcher.DEFAULT
GlobalScope.launch {
println("1.${Thread.currentThread().name}") //第一类3:默认线程池
delay(1000L)
println("2.${Thread.currentThread().name}") //第一类3:默认线程池
runBlocking {
println("6.${Thread.currentThread().name}") //第二类1:默认线程池
}
}
Executors.newSingleThreadExecutor().asCoroutineDispatcher().use { ct->
//指定了特定的上下文
GlobalScope.async(ct) {
println("3.${Thread.currentThread().name}") //第一类1:自定义现场
"SUCCESS"
}
}
//没有指定,同时没有
runBlocking {
println("4.${Thread.currentThread().name}") //当前的启动线程:main
//没有指定,但是有继承自 runBlocking 的上下文 main
launch {
println("5.${Thread.currentThread().name}") //继承上下文:main
}
delay(3000)
}
}
1.DefaultDispatcher-worker-1 @coroutine#1
3.pool-1-thread-1 @coroutine#2
4.main @coroutine#3
5.main @coroutine#4
2.DefaultDispatcher-worker-1 @coroutine#1
6.DefaultDispatcher-worker-1 @coroutine#5
协程是可以被取消的,同时父协程取消也会让对应的子协程一同被取消,通过调用cancel()
取消协程。
协程在每次resume
的时候,会判定一次是否能取消,比如下面代码
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L) //resume了一次,此时判定
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
println("main: Now I can quit.")
那么对于计算密集型任务,中间不会有resume
的时候,建议通过isActive
这个状态来判定是否需要取消。
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可以被取消的计算循环
//下面代码没有发生协程切换的操作,所以在不加isActive的情况下,也不会被取消,因为没有机会判定
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并等待它结束
println("main: Now I can quit.")
同时取消父协程会导致其下所有子协程的也被取消!
关于取消的其他相关知识,请参考地址:协程的取消
做界面开发,比如
Android
或者Swing
的朋友,用的比较多。
启动了某个协程,做完某个工作后,需要重新切换到另外一个上下文进行后续工作,怎么办?
此时withContext()
的作用就来了,我们看看下面的例子,这个例子在Android
的场景下非常适合:
suspend fun doSomeWork(): String {
println("do....")
delay(1000)
return "data..."
}
launch(Dispatchers.IO) {
val data=doSomeWork()
withContext(Dispatchers.Main){
label=data
}
}
doSomeWork
本来运行在IO
线程中的,获取结果后需要将数据渲染到UI
线程上。withContext(Dispatchers.Main)
这一段简单的代码就将协程切换到了UI
线程了。协程作用域就是将某些有关联的逻辑在同一个作用域内起作用,他们拥有同一个coroutineContext
,所以他们彼此有关系,这样可以统一来控制他们的取消。
该方法是创建协程作用域最常见的一个方法,创建的是底层的协程,通常起到线程
和协程
连接的作用:对外阻塞线程,对内等待协程结束。
比如如下代码就是在同一个协程作用域内,当某个协程出现问题后,可以统一的将其取消。
suspend fun showSomeData() = coroutineScope {
//1、通过 io 执行某些操作
val data = async(Dispatchers.IO) { // <- extension on current scope
... load some UI data for the Main thread ...
}
//2、在主线程进行展示
withContext(Dispatchers.Main) {
doSomeWork() //2.1 调用某个方法
val result = data.await() //2.2 等待 1中的方法的执行结果
display(result) //2.3 调用显示方法
}
}
coroutineScope
包裹的作用域
doSomeWork
或者获取 data 的过程,获取 display 的任何一个方法抛出异常,都会导致在该作用域内的协程被取消。coroutineScope
的情况下,异常会往上抛。 @Test
fun testSupervisor()= runBlocking {
//子协程互不相干
supervisorScope {
//协程 1
launch {
delay(200)
println("1")
}
//协程 2
launch {
delay(100)
throw RuntimeException("xxx")
println("2")
}
//协程 3
launch {
delay(300)
println("3")
}
}
return@runBlocking
}
在上述例子中,虽然协程 2 抛出了异常,但是不会导致协程 1 和协程 3 被取消,1 和 3 仍然会执行下去。
你可以将supervisorScope 改为 coroutineScope ,看看效果如何!
-Dkotlinx.coroutines.debug
,这样当我们利用代码Thread.currentThread().name
获取线程名的时候,会将协程名称也统一获取到,比如:main @coroutine#5
delay
不同于sleep
的一点在于,不会阻碍线程的执行,只是将协程挂起。