目录
一、第一个协程程序
二、结构化并发
三、提取函数重构
四、作用域构建器
五、协程句柄Job
六、协程很轻量
协程是一个可挂起的计算实例。从概念上讲,它与线程类似,因为它也运行一段代码,并且与其余代码并发执行。然而,协程并不绑定到任何特定的线程。它可以在一个线程中挂起执行,并在另一个线程中恢复执行。
协程可以被视为轻量级的线程,但它们之间存在一些重要的区别,这使得它们在实际使用中与线程有很大的不同。
运行以下代码来创建你的第一个可工作的协程:
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
//sampleEnd
代码运行的结果:
Hello
World!
让我们来剖析这段代码的作用:
如果在代码中删除或忘记使用 runBlocking,那么在调用 launch 时会出现错误,因为 launch 仅在 CoroutineScope 上声明:
Unresolved reference: launch
runBlocking 的名称意味着运行它的线程(在这种情况下是主线程)在调用期间会被阻塞,直到 runBlocking { ... } 内的所有协程都完成执行。你通常会看到 runBlocking 在应用程序的最顶层被这样使用,而在真正的代码中却很少见到它的身影,因为线程是宝贵的资源,阻塞它们是低效的,而且通常是不被期望的。
协程遵循结构化并发的原则,这意味着新的协程只能在特定的 CoroutineScope 内启动,该作用域限定了协程的生命周期。上面的例子表明,runBlocking 建立了相应的作用域,这就是为什么前一个例子会等待一秒延迟后打印出 "World!",然后才退出。
在真实的应用程序中,你会启动大量的协程。结构化并发确保它们不会被遗漏或泄露。一个外部作用域在其所有子协程完成之前不能完成。结构化并发还确保代码中的任何错误都能得到正确的报告,并且永远不会丢失。
让我们将 launch { ... }
内的代码块提取到一个单独的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend
修饰符的新函数。这是你的第一个挂起函数。挂起函数可以在协程内部像普通函数一样使用,但它们的额外功能是能够反过来使用其他挂起函数(如本例中的 delay
),以挂起协程的执行。
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
//sampleEnd
除了不同构建器提供的协程作用域外,还可以使用 coroutineScope
构建器声明自己的作用域。coroutineScope
创建一个协程作用域,并会一直等待,直到所有启动的子协程都完成。coroutineScope
实际上会挂起(suspend)调用它的那个协程。
runBlocking
和 coroutineScope
构建器可能看起来相似,因为它们都会等待其主体和所有子协程完成。主要区别在于,runBlocking
方法会阻塞当前线程以进行等待,而 coroutineScope
只是挂起,释放底层线程以供其他用途。正因为这种差异,runBlocking
是一个普通函数,而 coroutineScope
是一个挂起函数。
你可以从任何挂起函数中使用 coroutineScope
。例如,你可以将“Hello”和“World”的并发打印移动到挂起函数 doWorld()
中:
import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
//sampleEnd
执行结果为:
Hello
World!
五、作用域构建器与并发
coroutineScope
构建器可以在任何挂起函数内部使用,以执行多个并发操作。让我们在 doWorld
挂起函数内部启动两个并发协程:
import kotlinx.coroutines.*
//sampleStart
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
//sampleEnd
launch { ... }
块内的两段代码都会并发执行。从程序开始计时,首先会在1秒后打印出“World 1”,紧接着在2秒后打印出“World 2”。doWorld
函数中的 coroutineScope
只有在两个协程都完成后才会结束,因此 doWorld
函数只有在此时才会返回,并允许打印出“Done”字符串:
执行结果为:
Hello
World 1
World 2
Done
launch
协程构建器返回一个 Job
对象,这个对象是已启动协程的句柄,可以用来显式地等待其完成。例如,你可以等待子协程完成,然后打印出“Done”字符串:
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = 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
println("Done")
//sampleEnd
}
执行结果为:
Hello
World!
Done
协程相比JVM线程使用的资源更少。使用线程时可能导致JVM可用内存耗尽的代码,在使用协程时则可以避免达到资源限制。例如,下面的代码启动了50,000个不同的协程,每个协程等待5秒钟,然后打印一个点('.'),同时消耗非常少的内存:
fun main() = runBlocking {
repeat(50_000) { // 启动大量的协程
launch {
delay(5000L)
print(".")
}
}
}
如果你使用线程来编写相同的程序(移除runBlocking
,将launch
替换为thread
,并将delay
替换为Thread.sleep
),那么它将消耗大量内存。这取决于你的操作系统、JDK版本及其设置,程序可能会抛出内存溢出错误,或者线程启动速度会变得缓慢,以确保不会有太多线程同时运行。
推荐文章
Welcome to our tour of Kotlin! | Kotlin Documentation