协程介绍
协程 (Coroutines) 是一种轻量级的线程,协程提供了一种不阻塞线程,但是可以被挂起的计算过程。线程阻塞开销是巨大的,而协程挂起基本上没有开销。 在执行阻塞任务时,会将这种任务放到子线程中执行,执行完成再回调( callback )主线程,更新UI 等操作,这就是异步编程。协程底层库也是异步处理阻塞任务,但是这些复杂的操作被底层库封装起来,协程代码的程序流是顺序的,不再需要一堆的回调函数,就像同步代码一样,也便于理解、调试和开发。 线程是抢占式的,线程调度是操作系统级的。而协程是协作式的,而协程调度是用户级的,协程是用户空间线程,与操作系统无关,所以需要用户自己去做调度。第一个协程程序
协程是轻量级的线程,因此协程也是由主线程管理的,如果主线程结束那么协程也就结束了,使用 GlobalScope.launch { } 在主线程中,后台会启动一个新的协程并继续。因此在main()函数的测试代码中,需要写一些阻塞主线程的代码,否则主线程结束了协程也就没机会执行了。
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
输出结果:
Hello, World!
本质上,协程是轻量级的线程。 它们在某些 CoroutineScope 上下文中与 launch 协程构建器 一起启动。 这里我们在 GlobalScope 中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。
可以将 GlobalScope.launch { …… } 替换为 thread { …… },并将 delay(……) 替换为 Thread.sleep(……) 达到同样目的。 试试看(记得导入 kotlin.concurrent.thread)。
如果你首先将 GlobalScope.launch 替换为 thread,编译器会报以下错误:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
这是因为 delay 是一个特殊的挂起函数 ,它不会造成线程阻塞,但是会挂起协程,并且只能在协程中使用。
桥接阻塞与非阻塞的世界
第一个示例在同一段代码中混用了非阻塞的 delay(……) 与阻塞的 Thread.sleep(……)。 这容易让我们记混哪个是阻塞的、哪个是非阻塞的。 让我们显式使用 runBlocking 协程构建器来阻塞主线程:
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L)
println("World!")
}
println("Hello,") // 主线程中的代码会立即执行
runBlocking { // 但是这个表达式阻塞了主线程,等价于 Thread.sleep(2000L);
delay(2000L) // ……我们延迟 2 秒来保证 JVM 的存活
}
}
结果是相似的,但是这些代码只使用了非阻塞的函数delay。 调用了 runBlocking 的主线程会一直阻塞直到 runBlocking 内部的协程执行完毕。
这个示例可以使用更合乎惯用法的方式重写,使用runBlocking来包装 main函数的执行:
import kotlinx.coroutines.* fun main() = runBlocking{ // 开始执行主协程 GlobalScope.launch { // 在后台启动一个新的协程并继续 delay(1000L) println("World!") } println("Hello,") // 主协程在这里会立即执行 delay(2000L) // 延迟 2 秒来保证 JVM 存活 }
这里的 runBlocking
使用写成执行For循环:
import java.lang.Math.random import java.lang.Thread.sleep import kotlinx.coroutines.* fun main(args: Array) { GlobalScope.launch { //在主线程中,后台启动一个协程 执行以下的for循环 println("子协程执行开始:") for (i in 0..7) { // 打印for循环执行次数 println("for循环执行第${i}次") // 生成挂起时间 val sleepTime =1000L // 协程挂起 delay(sleepTime) } println("子协程执行结束。") } sleep(9000L) // 主线程休眠,保证JVM存活,保持其他线程处于活动状态 println("主协程结束。") }
或者显式使用 runBlocking 协程构建器来阻塞主线程,使用Kotlin风格写法来包装 main函数的执行:
import java.lang.Math.random import java.lang.Thread.sleep import kotlinx.coroutines.* fun main() = runBlocking{ // 开始执行主协程 GlobalScope.launch { //启动一个协程 println("子协程执行开始:") for (i in 0..7) { // 打印for循环执行执行次数 println("for循环执行第${i}次") // 生成挂起时间 val sleepTime =1000L // 协程挂起 delay(sleepTime) } println("子协程执行结束。") } delay(9000L) // 延迟 2 秒来保证 JVM 存活 println("主协程结束。") }
输出结果:
子协程执行开始: for循环执行第0次 for循环执行第1次 for循环执行第2次 for循环执行第3次 for循环执行第4次 for循环执行第5次 for循环执行第6次 for循环执行第7次 子协程执行结束。 主协程结束。
等待一个作业
以阻塞方式延迟一段时间来等待另一个协程运行并不是一个好的选择。让我们以非阻塞方式等待所启动的后台 Job 执行结束。
launch函数的返回是一个Job对象,Job是协程要执行的任务,可以将Job对象看做协程本身,所有对协程的操作都是通过Job对象完成的,协程的状态和生命周期都是通过Job反映出来的。
// --------------- launch ---------------
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
context参数是协程上下文对象默认在Dispatchers.Default,Dispatchers.Default通常是一个公共线程池。start参数设置协程启动,block参数是协程体,类似于线程体,协程执行的核心代码在此编写,在协程体中执行的函数应该都是挂起函数,例如delay函数就是挂起的。
提示 使用 kotlinx.coroutines 框架,开发人员不需要直接创建协程对象,而是使用Job 对象。 Job对象中常用的属性和函数如下: isActive 属性:判断 Job 是否处于活动状态。 isCompleted属性:判断Job 是否处于完成状态。 isCancelled属性:判断Job 是否处于取消状态。 start函数:开始Job 。 cancel函数:取消Job 。 join函数:是当前协程处于等待状态,直到Job 完成, join 是一个挂起函数只能在协程体中或其他的挂起函数中调用。
// This file was automatically generated from coroutines-basics.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.exampleBasic04
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
}
输出结果:
Hello, World!
现在,结果仍然相同,但是主协程与后台作业的持续时间没有任何关系了。
使用job输出for循环:
fun main() = runBlocking {
val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用
for(i in 0..7){
delay(1000L)
println("for循环执行第${i}次!")
}
println("World!")
}
println("Hello,")
println("job.isActive: ${job.isActive}")
println("job.isCompleted: ${job.isCompleted}")
job.join() // 等待直到子协程执行结束 join是一个挂起函数只能在协程体中或其他的挂起函数中调用
println("job.isCompleted: ${job.isCompleted}")
}
输出结果:
Hello, job.isActive: true job.isCompleted: false for循环执行第0次! for循环执行第1次! for循环执行第2次! for循环执行第3次! for循环执行第4次! for循环执行第5次! for循环执行第6次! for循环执行第7次! World! job.isCompleted: true
结构化的并发
协程的实际使用还有一些需要改进的地方。 当我们使用 GlobalScope.launch 时,我们会创建一个顶层协程。虽然它很轻量,但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样?(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样?必须手动保持对所有已启动协程的引用并 join 之,很容易出错。
有一个更好的解决办法。我们可以在代码中使用结构化并发。 我们可以在执行操作所在的指定作用域内启动协程, 而不是像通常使用线程那样在 GlobalScope 中启动。
在我们的示例中,我们使用 runBlocking 协程构建器将 main 函数转换为协程。 包括 runBlocking 在内的每个协程构建器都将 CoroutineScope 的实例添加到其代码块所在的作用域中。 我们可以在这个作用域中启动协程而无需显式 join 之,因为外部协程(示例中的 runBlocking)直到在其作用域中启动的所有协程都执行完毕后才会结束。因此,可以将我们的示例简化为:
// This file was automatically generated from coroutines-basics.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.exampleBasic05
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // 在 runBlocking 作用域中启动一个新协程
delay(1000L)
println("World!")
}
println("Hello,")
}
作用域构建器
除了由不同的构建器提供协程作用域之外,还可以使用 coroutineScope 构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。
runBlocking 与 coroutineScope 可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束。 主要区别在于:
① runBlocking 方法会阻塞当前线程来等待;
② coroutineScope 只是挂起,会释放底层线程用于其他用途;
由于存在这点差异,runBlocking 是常规函数,而 coroutineScope 是挂起函数。
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // 创建一个协程作用域
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // 这一行会在内嵌 launch 之前输出
}
println("Coroutine scope is over") // 这一行在内嵌 launch 执行完毕后才输出
}
输出结果:请注意,(当等待内嵌 launch 时)紧挨“Task from coroutine scope”消息之后, 就会执行并输出“Task from runBlocking”——尽管 coroutineScope 尚未结束。
Task from coroutine scope Task from runBlocking Task from nested launch Coroutine scope is over
提取函数重构
我们来将 launch { …… } 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend 修饰符的新函数。 这是你的第一个挂起函数。在协程内部可以像使用普通函数一样使用挂起函数。 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 delay)来挂起协程的执行。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// 这是你的第一个挂起函数
suspend fun doWorld() {
delay(1000L)
println("World!")
}
全局协程
以下代码在 GlobalScope 中启动了一个长期运行的协程,该协程每秒输出“I'm sleeping”两次,之后在主函数中延迟一段时间后返回。
package kotlinx.coroutines.guide.exampleBasic09
import kotlinx.coroutines.*
fun main() = runBlocking {
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1100L) // just quit after delay
}
你可以运行这个程序并看到它输出了以下三行后终止:
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ...
在 GlobalScope 中启动的活动协程并不会使进程保活,这又回到了最开始的实例。它们就像守护线程。



