Kotlin协程详解——协程基础

目录

一、第一个协程程序

二、结构化并发

三、提取函数重构

四、作用域构建器

五、协程句柄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!

让我们来剖析这段代码的作用:  

  • launch 是一个协程构建器。它会启动一个新的协程,与代码的其余部分并发运行,而其余代码会继续独立执行。这就是为什么 Hello 会首先被打印出来。
  • delay 是一个特殊的挂起函数。它会使协程挂起指定的时间。挂起协程并不会阻塞底层线程,而是允许其他协程运行并使用底层线程来执行它们的代码。
  • runBlocking 也是一个协程构建器,它充当了常规函数 main() 的非协程世界与 runBlocking { ... } 大括号内包含协程的代码之间的桥梁。这在集成开发环境(IDE)中通过 runBlocking 开括号后的 CoroutineScope 提示来突出显示。

如果在代码中删除或忘记使用 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

五、协程句柄Job

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

你可能感兴趣的:(Kotlin,kotlin,android)