协程基础(官方文档)
这部分主要包括协程的一些基本概念。
目录
1、你的第一个协程
尝试运行下面的代码:
package com.cool.cleaner.test
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
GlobalScope.launch {
delay(1000)//毫秒
print("World!\n")
}
println("Hello, ")
Thread.sleep(2000)
}
下面是上面程序的输出:
Hello,
World!
Process finished with exit code 0
本质上协程属于轻量级线程,它们是在一个包含协程上下文的协程作用域(CoroutineScope)中使用launch函数启动的;上面的代码中我们使用GlobalScope启动了一个协程,这意味着新启动的协程的生命周期只受到整个应用程序生命周期的限制。
你可以使用thread{...}、delay(...)并配合Thread.sleep(...)也能达到相应的效果,你可以试试看(别忘了导入kotlin.concurrent.thread)。
把GlobalScope.launch
替换成thread后,编译器会提示如下报错:
这是为啥呢?刚刚还好好的,其实主要是因为delay是一个suspend函数,它不会阻塞线程而是挂起协程,而最主要的是因为它只能从协程作用域调用(从协程中调用)。
2、贯通阻塞与非阻塞世界
上面的例子把阻塞delay(...)与非阻塞(Thread.sleep(...))的代码混在一起了,这让我们容易搞混哪个是阻塞、哪个是非阻塞。这里可以使用runBlocking这个协程构建器显示的表明需要阻塞式运行,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() {
GlobalScope.launch {//启动一个新协程
delay(1000)
print("World!\n")
}
println("Hello, ")//主线程立即执行到这里
runBlocking {//runBlocking将会阻塞主线程
delay(2000)//延时2s保证JVM还活着
}
}
输出也是一样的,但是这里只使用了非阻塞式的代码delay,主线程中调用了runBlocking,而runBlocking将会阻塞直到它内部的协程执行完成。
这个例子还可以使用一种更常用的方式来写,那就是使用runBlocking来封装主函数的执行,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启动主协程
GlobalScope.launch {//启动一个新协程
delay(1000)
print("World!\n")
}
println("Hello, ")//主线程立即执行到这里
delay(2000)//延时2s保证JVM还活着
}
这里的runBlocking<Unit> { ... }用作启动顶级协程的一个适配器,这里显示的把Unit作为返回类型,是因为在kotlin中一个合乎语法的习惯是将main函数的返回类型设置为Unit。
下面是一种为suspend函数编写单元测试的方式:
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// here we can use suspending functions using any assertion style that we like
}
}
3、等待执行结果
当其它协程还在执行的时候,使用延时等待其执行的方式并不是一种好的方式,我们可以显示的等待(以一种非阻塞的方式)直到我们启动的协程执行任务完成,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启动主协程
val job = GlobalScope.launch {//启动一个新协程
delay(1000)
print("World!\n")
}
println("Hello, ")//主线程立即执行到这里
job.join()//等待子协程执行完成
}
运行结果和上面的一致,但是现在主协程的代码并不会与后台任务的执行时间绑定在一起(显示指定等待多久),这种方式更好一些。
4、结构化并发
在实际使用协时有一种情况,当我们使用GlobalScope.launch的时候其实我们是创建了一个顶级协程,即使它是轻量,但是当它执行执行 时候它也会消耗消耗一些资源;假如我们忘记保持它的一个引用而没有在合适的时候结束它,那它就会继续运行;假如协程中的代码挂起了呢?(比如:我们错误的延时了很久),或者我们启动了太多协程而内存溢出了呢?让程序员自己追踪所有的协程引用并在合适时候释放资源被证明是一种错误的编码方式。
其实有一种更好的解决方案,我们可以在代码中使用结构化并发,而不是像使用线程(线程是全局的)一样在GlobalScope中启动协程,我们可以在要执行任务的使用域内启动协程。
在我们的例子中,我们使用runBlocking协程构建器把主函数转变成了一个协程,每一个协程构建器包括runBlocking都会在它的代码块内添加一个CoroutineScope(协程作用域),我们可以在这个作用域内启动协程而不用显示的join,因为外部的协程(例子中的runBlocking)只有等到它内部启动的协程执行完成后才会结束;因此我们的例子还可以更简洁,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启动主协程
launch {//启动一个新协程
delay(1000)
print("World!\n")
}
println("Hello, ")//主线程立即执行到这里
}
5、协程作用域构建器(Scope builder)
除了使用构建器创建协程作用域,你也可以使用coroutineScope构建器创建你自己的协程作用域,它会创建一个等待所有子协程执行完成才结束的协程作用域。
runBlocking和coroutineScope看起来很像,因为它们都会等待函数块及子协程执行完成,其实他们最主要的区别是runBlocking方法会阻塞当前线程而等待,而coroutineScope则只是挂起并释放线程以作它用。因为这样所以runBlocking是一个普通函数而coroutineScope则是一个挂起函数。
这可以用如下的代码来证明:
package com.cool.cleaner.test
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启动主协程
launch {
delay(200L)
println("Task from runBlocking")//2
}
coroutineScope {//协程作用域
launch {
delay(500)
println("Task from nested launch")//3
}
delay(100)//1
println("Task from coroutine scope")
}
println("Coroutine scope is over")//4
}
你可以想上面的程序输出顺序是怎样的,以下是输出结果:
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
Process finished with exit code 0
为什么是这样的呢?其实主要是因为coroutineScope会等待其内部的代码块和协程执行完成才会继续执行后面的代码。
6、函数提取
这里我们可以把代码块launch { ... }中的代码抽取到一个独立的函数suspend中,该函数使用suspend修饰,这是你编写的第一个suspend函数,suspend函数可以在协程中使用,但是它的另一个特性是在它的代码中也可以使用其它的suspend函数,代码如下:
package com.cool.cleaner.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启动主协程
launch {
doWorld()
}
println("Hello, ")
}
suspend fun doWorld(): Unit {
delay(1000)
println("World!")
}
7、协程是轻量的
运行如下代码:
package com.cool.cleaner.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启
repeat(100_000) {
launch {
delay(5000)
print(".")
}
}
}
启动十万个协程,5秒后,每个协程都打印一个"."。
你现在可以使用线程试试将会发生什么?(大部分情况是Out-Of-Memory的错误)
8、全局协程
下面的代码在GlobalScope范围内启动一个长时间运行的协程,并且每1秒内打印"I'm sleeping" 两次,当主协程运行结束后它退出了:
package com.cool.cleaner.test
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {//启
GlobalScope.launch {
repeat(1000) {index ->
println("I'm sleeping $index")
delay(500)
}
}
delay(1300)
}
下面是打印结果输出:
I'm sleeping 0
I'm sleeping 1
I'm sleeping 2
Process finished with exit code 0
你可以看到它只打印了3次,在GlobalScope中启动的协程并不会让JVM活着,如果你了解后台线程的话,你就会明白它就像后台线程一样,当主线程结束后,它就会突然终止。