第一个协程程序:使用 runBlocking、launch、delay,理解协程的基本执行流程
Kotlin 协程的入门,往往从三个最基础的 API 开始:runBlocking、launch 和 delay。这三个函数构成了协程世界的“第一块积木”——它们分别解决了“如何启动协程”、“如何创建子协程”以及“如何模拟耗时操作”的问题。理解这三者的协作关系,是后续掌握结构化并发、调度器切换、异常处理等高级主题的基石。本章将通过一个最小可运行的程序,拆解协程从启动到结束的完整生命周期,并深入剖析其背后的执行流程与线程模型。
上图展示了本章核心程序在时间轴上的执行轨迹。主线程进入 runBlocking 后,会创建一个协程作用域并阻塞当前线程,直到作用域内所有协程执行完毕。而 launch 启动的子协程则会在遇到 delay 时挂起,将线程释放给其他任务,待延时结束后再恢复执行。这一“阻塞-挂起-恢复”的机制,正是协程高效利用线程的关键。
核心原理:协程的“非阻塞式挂起”与结构化并发
要理解第一个协程程序,必须先厘清三个核心概念:协程作用域(CoroutineScope)、挂起函数(Suspending Function)以及结构化并发(Structured Concurrency)。
runBlocking 是一个特殊的协程构建器,它会创建一个新的协程作用域,并阻塞当前线程直到该作用域内的所有协程完成。这意味着,当你在主函数中调用 runBlocking 时,主线程会停下来等待,直到 runBlocking 块内的所有代码(包括所有子协程)执行完毕。这为学习协程提供了一个“安全”的入口——它保证了程序不会在协程完成前退出。
launch 则是一个协程构建器,它会在当前协程作用域内启动一个新的子协程。与 runBlocking 不同,launch 是非阻塞的,它会立即返回一个 Job 对象,而不会等待协程体执行完毕。子协程的执行是异步的,它会在后台调度执行。
delay 是一个挂起函数,它会让当前协程挂起(suspend)指定的时间,但不会阻塞当前线程。这是协程与线程最重要的区别之一:线程的 Thread.sleep() 会阻塞线程,而 delay 只会挂起协程,释放底层线程去执行其他任务。
结构化并发意味着每个协程的生命周期都被限制在其父协程的作用域内。父协程会等待所有子协程完成,如果父协程被取消,子协程也会被自动取消。这种机制避免了协程泄漏,使得并发代码更加健壮。
操作流程:从零编写并运行第一个协程程序
下面我们通过一个完整的示例,逐步演示如何编写和运行第一个协程程序。请在你的开发环境中(如 IntelliJ IDEA 或 Android Studio)新建一个 Kotlin 文件,并输入以下代码:
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start: ${Thread.currentThread().name}")
// 启动一个子协程
launch {
println("World: ${Thread.currentThread().name}")
delay(1000) // 挂起 1 秒,不阻塞线程
println("World after delay: ${Thread.currentThread().name}")
}
println("End: ${Thread.currentThread().name}")
}
运行这段代码,你会在控制台看到类似如下的输出(线程名可能因环境而异):
Start: main
End: main
World: main
World after delay: main
注意输出顺序:End 在 World 之前打印。这是因为 launch 启动的协程是异步执行的,主协程(即 runBlocking 块内的代码)会继续执行,不会等待子协程。当主协程执行完 println("End") 后,由于 runBlocking 会等待所有子协程完成,所以程序不会立即退出,而是等待子协程完成 delay(1000) 后打印 World after delay。
为了更清晰地观察执行流程,我们可以在代码中添加更多的打印语句和线程信息:
fun main() = runBlocking {
println("1. runBlocking 开始,线程: ${Thread.currentThread().name}")
launch {
println("2. launch 子协程开始,线程: ${Thread.currentThread().name}")
delay(1000)
println("3. delay 结束,线程: ${Thread.currentThread().name}")
}
println("4. runBlocking 继续执行,线程: ${Thread.currentThread().name}")
// 这里可以添加 Thread.sleep(2000) 来观察主线程是否被阻塞
}
运行后,输出顺序为:1 → 4 → 2 → 3。这清晰地展示了:主协程先执行到 launch 处,但 launch 只是“注册”了一个子协程,立即返回;主协程继续执行 println("4");随后子协程开始执行,遇到 delay 挂起;1 秒后子协程恢复,打印 3。
案例拆解:模拟网络请求的并发执行
为了将上述概念应用到更实际的场景中,我们模拟一个“并发请求两个 API 并汇总结果”的案例。假设我们有一个电商应用,需要同时获取用户信息和商品列表,然后合并展示。
首先,定义两个挂起函数来模拟网络请求:
suspend fun fetchUserInfo(): String {
delay(800) // 模拟网络延迟
return "用户信息: 张三"
}
suspend fun fetchProductList(): String {
delay(1200) // 模拟网络延迟
return "商品列表: [手机, 电脑, 耳机]"
}
然后,在 runBlocking 中使用两个 launch 并发执行这两个请求:
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
var userInfo = ""
var productList = ""
// 并发启动两个子协程
val job1 = launch { userInfo = fetchUserInfo() }
val job2 = launch { productList = fetchProductList() }
// 等待两个协程都完成
job1.join()
job2.join()
val endTime = System.currentTimeMillis()
println("耗时: ${endTime - startTime} ms")
println("结果: $userInfo, $productList")
}
运行结果:
耗时: 1205 ms
结果: 用户信息: 张三, 商品列表: [手机, 电脑, 耳机]
如果使用传统的顺序执行(先请求用户信息,再请求商品列表),总耗时将是 800 + 1200 = 2000ms。而使用协程并发执行,总耗时仅为 1205ms(取两者中较长的耗时),效率提升了近 40%。这个案例展示了协程在 I/O 密集型任务中的巨大优势。
值得注意的是,job1.join() 和 job2.join() 是挂起函数,它们会挂起当前协程,直到对应的子协程完成。这保证了在打印结果时,两个变量都已经被赋值。
常见误区与总结复盘
在初学协程时,有几个常见的误区需要特别警惕:
误区一:认为 launch 会阻塞当前线程。 实际上,launch 是非阻塞的,它会立即返回。如果子协程中没有挂起函数,它会一直执行到完成,但主协程不会等待它。
误区二:混淆 delay 和 Thread.sleep()。 delay 是挂起函数,只挂起当前协程,不阻塞线程;而 Thread.sleep() 会阻塞整个线程。在协程中使用 Thread.sleep() 会浪费线程资源,并可能导致性能问题。
误区三:忘记 runBlocking 会阻塞线程。 runBlocking 通常只用于主函数或测试代码中,如果在 Android 的 UI 线程中使用,会导致界面卡顿(ANR)。
为了巩固理解,我们总结本章的核心要点:
| API | 类型 | 是否阻塞线程 | 返回值 | 典型用途 |
|---|---|---|---|---|
runBlocking |
协程构建器 | 是(阻塞当前线程) | T(块内最后一行) |
主函数入口、测试代码 |
launch |
协程构建器 | 否(非阻塞) | Job |
启动一个无需返回结果的异步任务 |
delay |
挂起函数 | 否(仅挂起协程) | 无 | 模拟耗时操作、定时任务 |
最后,请记住协程执行的核心流程:启动(runBlocking/launch)→ 执行 → 挂起(delay/其他挂起函数)→ 恢复 → 完成。这个流程贯穿于所有协程程序中,理解它是掌握协程调度机制的关键。在下一阶段的学习中,你将接触到 async/await、Dispatchers 等更高级的工具,但它们的底层逻辑都建立在本章所学的这三个基础 API 之上。
复盘建议: 动手修改本章示例中的 delay 时长、增加多个 launch 子协程,观察输出顺序的变化。尝试在 launch 块内使用 Thread.sleep(),对比与 delay 的行为差异。通过实验,你将更深刻地理解协程的挂起与恢复机制。