Kotlin协程定义:
从广义上来讲,协程就代表了“互相协作的程序”,也就是“Cooperative-routine”。
协程框架,是独立于 Kotlin 标准库的一套框架,它封装了 Java 的线程,对开发者暴露了协程的 API。
协程可以理解为运行在线程当中的、更加轻量的 Task。
一个线程当中,可以运行成千上万个协程;
协程,也可以理解为运行在线程当中的非阻塞的 Task;
协程,通过挂起和恢复的能力,实现了“非阻塞”;
协程不会与特定的线程绑定,它可以在不同的线程之间灵活切换,而这其实也是通过“挂起和恢复”来实现的。
在 Kotlin 当中,协程是一个独立的框架。跟 Kotlin 的反射库类似,协程并不是直接集成在标准库当中的。如果想要使用 Kotlin 的协程,就必须手动进行依赖:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
Kotlin 官方之所以将协程作为一个单独的框架独立出来,也是为了减小标准库的体积,给开发者更多的灵活性。
协程库源码:github.com/Kotlin/kotl…
协程调试:设置 VM 参数、断点调试
1、在运行/调试配置中将 VM 参数设置成“-Dkotlinx.coroutines.debug”
完成这个设置后,当我们在 log 当中打印“Thread.currentThread().name”的时候,如果当前代码是运行在协程当中的,那么它就会带上协程的相关信息。@coroutine#1”就代表了 launch 创建的协程。
2、断点调试协程
确保 IDE 自带的 Kotlin 编译器插件版本号大于 1.4,为协程代码打断点,并且右击断点处,勾选 suspend、All,这代表断点将会对协程生效。当程序停留到断点处以后,需要确保协程调试窗口已经被开启:
在这个专属的协程调试窗口当中,可以看到很多有用的协程信息,包括:当前协程的名字,这里是“coroutine#1”;当前协程运行在哪个线程之上,这里是“DefaultDispatcher-worker-1”;当前协程的运行状态,这里是“RUNNING”;当前协程的“创建调用栈”。
启动协程的三种方式
1、launch
是典型的“Fire-and-forget”场景,它不会阻塞当前程序的执行流程,使用这种方式的时候,我们无法直接获取协程的执行结果。它有点像是生活中的射箭。
launch使用:
/* delay 函数的定义
注意这个关键字
↓ */
public suspend fun delay(timeMillis: Long) { ... }
// 仅用于研究,生产环境不建议使用GlobalScope
fun main() {
// ①
GlobalScope.launch {
// ②
delay(1000L)
println("Hello World!")
}
// ③
Thread.sleep(2000L)
}
/*
输出结果;
Hello World!
*/
注释①,GlobalScope.launch{},它是一个高阶函数,它的作用就是启动一个协程。GlobalScope 是 Kotlin 官方为我们提供的“协程作用域”,这涉及到协程的“结构化并发”理念。
注释②,delay(),它的作用就是字面上的意思,“延迟”。以上代码中,我们是延迟了 1 秒。从 delay() 的函数签名这里可以发现,它的定义跟普通的函数不太一样,它多了一个“suspend”关键字,这代表了它是一个挂起函数。而这也就意味着,delay 将会拥有“挂起和恢复”的能力。
注释③,它的作用是让当前线程休眠 2 秒钟。
launch 函数的源代码:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job { ... }
CoroutineScope.launch(),代表了 launch 其实是一个扩展函数,而它的“扩展接收者类型”是 CoroutineScope。这就意味着,我们的 launch() 会等价于 CoroutineScope 的成员方法。而如果我们要调用 launch() 来启动协程,就必须要先拿到 CoroutineScope 的对象。前面的案例,我们使用的 GlobalScope,其实就是 Kotlin 官方为我们提供的一个 CoroutineScope 对象,方便我们开发者直接启动协程。
第一个参数:CoroutineContext,它代表了我们协程的上下文,它的默认值是 EmptyCoroutineContext,如果我们不传这个参数,默认就会使用 EmptyCoroutineContext。一般来说,我们也可以传入 Kotlin 官方为我们提供的 Dispatchers,来指定协程运行的线程池。协程上下文,是协程当中非常关键的元素
第二个参数:CoroutineStart,它代表了协程的启动模式。如果我们不传这个参数,它会默认使用 CoroutineStart.DEFAULT。CoroutineStart 其实是一个枚举类,一共有:DEFAULT、LAZY、ATOMIC、UNDISPATCHED。我们最常使用的就是 DEFAULT、LAZY,它们分别代表:立即执行、懒加载执行。
最后一个参数,是一个函数类型的 block,它的类型是“suspend CoroutineScope.() -> Unit”。它是一个“挂起函数”,同时,它还应该是 CoroutineScope 类的成员方法或是扩展方法.
2、runBlocking
runBlocking,我们可以获取协程的执行结果,但这种方式会阻塞代码的执行流程,因为它一般用于测试用途,生产环境当中是不推荐使用的。
使用:
fun main() {
runBlocking { // 1
println("Coroutine started!") // 2
delay(1000L) // 3
println("Hello World!") // 4
}
println("After launch!") // 5
Thread.sleep(2000L) // 6
println("Process end!") // 7
}
/*
输出结果:
Coroutine started!
Hello World!
After launch!
Process end!
*/
使用 runBlocking 启动的协程会阻塞当前线程的执行,这样一来,所有的代码就变成了顺序执行:1、2、3、4、5、6、7。这其实就是 runBlocking 与 launch 的最大差异。
runBlocking 确实会阻塞当前线程的执行。对于这一点,Kotlin 官方也强调了:runBlocking 只推荐用于连接线程与协程,并且,大部分情况下,都只应该用于编写 Demo 或是测试代码。所以,请不要在生产环境当中使用 runBlocking。
runBlocking 的函数签名:
public actual fun <T> runBlocking(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T): T {
...
}
runBlocking 就是一个普通的顶层函数,它并不是 CoroutineScope 的扩展函数,因此,我们调用它的时候,不需要 CoroutineScope 的对象。另外,可以注意到它的第二个参数“suspend CoroutineScope.() -> T”,这个函数类型是有返回值类型 T 的,而它刚好跟 runBlocking 的返回值类型是一样的。因此,可以推测,runBlocking 其实是可以从协程当中返回执行结果的。
fun main() {
val result = runBlocking {
delay(1000L)
// return@runBlocking 可写可不写
return@runBlocking "Coroutine done!"
}
println("Result is: $result")
}
/*
输出结果:
Result is: Coroutine done!
*/
从表面上看,runBlocking 是对 launch 的一种补充,但由于它是阻塞式的,因此,runBlocking 并不适用于实际的工作当中。
3、async
是很多编程语言当中普遍存在的协程模式。它像是结合了 launch 和 runBlocking 两者的优点。它既不会阻塞当前的执行流程,还可以直接获取协程的执行结果。它有点像是生活中的钓鱼。
使用 async{} 创建协程,并且还能通过它返回的句柄拿到协程的执行结果。举例:
fun main() = runBlocking {
println("In runBlocking:${Thread.currentThread().name}")
val deferred: Deferred<String> = async {
println("In async:${Thread.currentThread().name}")
delay(1000L) // 模拟耗时操作
return@async "Task completed!"
}
println("After async:${Thread.currentThread().name}")
val result = deferred.await()
println("Result is: $result")
}
/*
输出结果:
In runBlocking:main @coroutine#1
After async:main @coroutine#1 // 注意,它比“In async”先输出
In async:main @coroutine#2
Result is: Task completed!
*/
直接使用 runBlocking 来实现 main 函数。由于 runBlocking 的最后一个参数的类型是“suspend CoroutineScope.() -> T”,因此在 Lambda 当中已经有了 CoroutineScope,所以我们可以直接在 runBlocking 当中,用 async 启动一个协程。从程序的输出结果,我可以看到,确实存在两个协程,runBlocking 启动的叫做“coroutine#1”;async 启动的叫做“coroutine#2”。
另外一个细节,那就是 async 启动协程以后,它也不会阻塞当前程序的执行流程,因为:“After async”在“In async”的前面就已经输出了。
还有,请注意 async{}的返回值,它是一个 Deferred 对象,我们通过调用它的 await() 方法,就可以拿到协程的执行结果。对比前面 launch 我们举的“射箭”的例子,这里的 async,就更加像是“钓鱼”:在我们钓鱼的时候,我们手里的鱼竿,就有点像是 async 当中的 Deferred 对象。只要我们手里有这根鱼竿,一旦有鱼儿上钩了,我们就可以直接拿到结果。
看看 async 的函数签名,顺便对比一下它跟 launch 之间的差异:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit // 不同点1
): Job {} // 不同点2
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T // 不同点1
): Deferred<T> {} // 不同点2
可以发现 launch 和 async 的两个不同点,一个是 block 的函数类型,前者的返回值类型是 Unit,后者则是泛型 T;另外一个不同点在返回值上,前者返回值类型是 Job,后者返回值类型是 Deferred。而 async 可以返回协程执行结果的原因也在于此。