Kotlin 协程闯关:看代码,猜结果

作者:潜龙勿用之化骨龙日期:2026/9/21

前言

这不是一篇 API 速查表。

接下来 10 关,每一关都先别急着运行。

先凭直觉判断,再打开 IntelliJ IDEA 验证,最后回头看协程到底是怎么运行的。

闯关规则

  1. 不要直接复制代码运行,先预测结果。
  2. 尽量不要查 API,看看自己对协程调度、挂起和生命周期到底掌握了多少。
  3. 在 IntelliJ IDEA 中逐关运行验证。
  4. 对照解析,看看自己能走到第几关。

Kotlin 协程闯关地图

10 关协程挑战

launchdelay,一路闯到结构化并发与生命周期。

关卡核心挑战
Level 01launch:到底是不是异步?
Level 02delay:到底会不会切线程?
Level 03join:到底在等什么?
Level 04async:并发执行,顺序 await
Level 05withContext:到底干了什么?
Level 06suspend:如何响应取消?
Level 07CPU 密集型任务:取消为何失效?
Level 08结构化并发:谁在等待谁?
Level 09作用域嵌套:任务如何结束?
Level 10BOSS 战:协程生命周期综合实战

Level 01:launch 是异步的吗?

难度:

核心考点: launch 的启动方式与 runBlocking 的等待机制

1. 看代码

1fun main() = runBlocking {
2    println("A")
3    launch {
4        println("B")
5    }
6    println("C")
7}
8

2. 先猜

下面哪一种输出是正确的?

  • A → B → C
  • A → C → B
  • B → A → C

先别运行。


3. 运行验证

1A
2C
3B
4

4. 为什么?

这里最容易产生一个误区:

launch 是异步的,所以一定会立刻跑到另一个线程?

不是。

在这个例子里,runBlocking 默认使用当前线程执行协程。launch 创建子协程后,调用本身不会等待子协程执行完成,因此当前协程可以继续向下执行,先打印 C

随后,子协程得到执行机会,打印 B

但程序并不会因为 runBlocking 已经执行到末尾就立刻退出。

runBlocking 会等待自己的子协程完成。

所以最终顺序是:

1A
2C
3B
4

这里应该记住第一条:

launch 不等于切线程,也不等于立即并行执行。它首先意味着:创建一个子协程,并立即返回一个 Job


Level 02:delay 到底会不会切线程?

难度: ⭐⭐

核心考点: delay 的挂起与线程阻塞

1. 看代码

1fun main() = runBlocking {
2    println("1: ${Thread.currentThread().name}")
3    launch {
4        println("2: ${Thread.currentThread().name}")
5        delay(1000)
6        println("3: ${Thread.currentThread().name}")
7    }
8    println("4: ${Thread.currentThread().name}")
9}
10

2. 先猜

两个问题:

  1. 23 打印出来的线程名称一定一样吗?
  2. delay(1000) 会不会阻塞当前线程 1 秒?

3. 运行验证

通常可以看到:

11: main
24: main
32: main
43: main
5

4. 为什么?

delay() 最重要的特点不是“等一秒”。

而是:

等待一秒,但不阻塞线程。

调用 delay(1000) 后,当前协程会挂起。

线程并没有因此被 Thread.sleep() 一样卡住。

在这个例子里,runBlocking 使用当前线程执行,因此协程恢复后仍然可能由 main 线程继续执行。

所以:

1delay  Thread.sleep
2

两者都可以“等一段时间”,但机制完全不同。

Thread.sleep()

1线程停在那里
2
3什么也干不了
4

delay()

1协程挂起
2
3线程可以继续执行其他工作
4
5时间到了
6
7协程恢复
8

需要注意:

挂起不代表一定换线程。

这是理解 Kotlin 协程时非常重要的一点。


Level 03:join 到底在等什么?

难度: ⭐⭐⭐

核心考点: Job.join() 的挂起等待

1. 看代码

1fun main() = runBlocking {
2    val job = launch {
3        delay(1000)
4        println("B")
5    }
6    println("A")
7    job.join()
8    println("C")
9}
10

2. 先猜

输出顺序是什么?

  • A → C → B
  • A → B → C

3. 运行验证

1A
2B
3C
4

4. 为什么?

关键就在这一句:

1job.join()
2

join() 的意思可以简单理解为:

等这个 Job 执行完成。

但它不是:

1Thread.sleep(...)
2

也不是把线程堵在那里。

join() 是挂起当前协程。

于是执行流程变成:

1打印 A
2   
3等待 job
4   
5当前协程挂起
6   
7job 执行
8   
9job 完成
10   
11当前协程恢复
12   
13打印 C
14

所以最终:

1A
2B
3C
4

这里可以顺便建立一个很重要的认知:

协程中的“等待”,很多时候等待的是任务状态,而不是等待线程。


Level 04:async 与 launch 的并发陷阱

难度: ⭐⭐⭐

核心考点: async 的启动时机与 await() 的挂起

我们来比较两段代码。


1. 代码段一

1fun main() = runBlocking {
2    val time = measureTimeMillis {
3        val a = async {
4            delay(1000)
5            100
6        }
7        val b = async {
8            delay(1000)
9            200
10        }
11        println("Result = ${a.await() + b.await()}")
12    }
13    println("Cost: $time ms")
14}
15

2. 代码段二

1fun main() = runBlocking {
2    val time = measureTimeMillis {
3        val a = async {
4            delay(1000)
5            100
6        }
7        val resultA = a.await()
8        val b = async {
9            delay(1000)
10            200
11        }
12        val resultB = b.await()
13        println("Result = ${resultA + resultB}")
14    }
15    println("Cost: $time ms")
16}
17

3. 先猜

两段代码最终都是:

1Result = 300
2

但耗时一样吗?

  • 代码段一:约 1000ms
  • 代码段二:约 2000ms

4. 运行验证

实际运行时会存在调度、机器负载等误差,但大致可以观察到:

1代码段一:≈ 1000ms
2代码段二:≈ 2000ms
3

5. 为什么?

第一段:

1val a = async { ... }
2val b = async { ... }
3

创建 a 后,代码继续创建 b

因此两个任务已经同时开始执行。

可以理解成:

1a ──────────────── 1000ms
2b ──────────────── 1000ms
3

最后:

1a.await()
2b.await()
3

只是获取结果。

所以整体大约需要:

11000ms
2

第二段就不一样了:

1val a = async { ... }
2val resultA = a.await()
3

这里已经开始等待 a

只有 a 完成以后,代码才会继续创建 b

于是变成:

1a ───────── 1000ms
2             
3b ───────── 1000ms
4

最终约:

12000ms
2

所以:

async 负责启动并发任务,await() 负责等待并获取结果。

而把 asyncawait() 紧紧挨在一起,很容易把并发写成串行。


Level 05:withContext 到底干了什么?

难度: ⭐⭐⭐⭐

核心考点: CoroutineContext 切换与线程调度

1. 看代码

1fun main() = runBlocking {
2    println("A: ${Thread.currentThread().name}")
3    val result = withContext(Dispatchers.Default) {
4        println("B: ${Thread.currentThread().name}")
5        delay(1000)
6        println("C: ${Thread.currentThread().name}")
7        100
8    }
9    println("D: $result, Thread: ${Thread.currentThread().name}")
10}
11

2. 先猜

两个问题:

  1. withContext 会不会创建一个新的协程?
  2. D 打印时的线程名称是否和 A 一致?

3. 运行验证

通常可以看到类似:

1A: main
2B: DefaultDispatcher-worker-1
3C: DefaultDispatcher-worker-1
4D: 100, Thread: main
5

4. 为什么?

这里不要简单把 withContext 理解成:

“开启一个新协程。”

更准确地说:

withContext 挂起当前协程,并在新的 CoroutineContext 中执行代码块,代码块完成后再恢复当前协程。

这里:

1withContext(Dispatchers.Default) {
2    ...
3}
4

意味着进入 Dispatchers.Default 上下文。

因此 BC 通常会在 Default dispatcher 的线程上执行。

代码块结束后:

1withContext(...)
2

返回 100,当前协程继续恢复到原来的上下文,所以 D 又回到了 main

可以把它理解成:

1main
2 
3withContext(Default)
4 
5Default worker
6 
7代码执行完成
8 
9恢复原来的上下文
10 
11main
12

所以:

withContext 的核心不是“切线程”,而是“切换 CoroutineContext”。线程变化只是调度器变化可能带来的结果。


Level 06:suspend 如何响应取消?

难度: ⭐⭐⭐⭐

核心考点: 协作式取消

这一关开始,真正进入协程生命周期。

1. 看代码

1fun main() = runBlocking {
2    val job = launch {
3        repeat(10) {
4            println("working $it")
5            delay(500)
6        }
7    }
8    delay(1200)
9    println("cancel")
10    job.cancel()
11    println("cancel done")
12}
13

2. 先猜

调用:

1job.cancel()
2

之后,子协程还能继续打印吗?

一共会打印几个 working


3. 运行验证

通常可以看到:

1working 0
2working 1
3working 2
4cancel
5cancel done
6

4. 为什么?

时间轴大致是:

1T = 0ms
2working 0
3    
4delay(500)
5
6T = 500ms
7working 1
8    
9delay(500)
10
11T = 1000ms
12working 2
13    
14delay(500)
15
16T = 1200ms
17cancel
18job.cancel()
19

此时子协程正处于:

1delay(500)
2

delay() 是支持取消的挂起函数。

因此取消发生后,delay() 会响应取消,子协程结束。

所以不会再出现:

1working 3
2

这里真正应该记住的是:

取消不是强制杀死协程,而是协程与取消机制之间的一次协作。


Level 07:CPU 密集型任务,取消为什么失效?

难度: ⭐⭐⭐⭐⭐⭐

核心考点: 非挂起代码与协作式取消

1. 看代码

把上一关的 delay() 换成:

1Thread.sleep()
2
1fun main() = runBlocking {
2    val job = launch(Dispatchers.Default) {
3        repeat(5) {
4            println("working $it")
5            Thread.sleep(500)
6        }
7    }
8    delay(700)
9    println("cancel")
10    job.cancel()
11    println("cancel done")
12}
13

2. 先猜

调用:

1job.cancel()
2

之后:

  1. 子协程会立刻停止吗?
  2. 最终会打印几个 working

3. 运行验证

你可能看到类似:

1working 0
2working 1
3cancel
4cancel done
5working 2
6working 3
7working 4
8

4. 为什么?

这里恰恰暴露出了协程取消最容易被误解的地方:

cancel() 不会强行终止正在执行的普通代码。

Thread.sleep(500) 是一个阻塞调用。

协程在执行:

1Thread.sleep(500)
2

的时候,并没有挂起,也没有主动检查取消状态。

所以即使其他地方调用:

1job.cancel()
2

当前这段代码依然可能继续执行。

这就是:

协作式取消。

如果是 CPU 密集型循环,则通常需要主动检查:

1while (isActive) {
2    // CPU work
3}
4

或者在合适的位置调用:

1yield()
2

让协程有机会检查取消状态。

因此:

1cancel()
2    
3发送取消信号
4    
5协程是否停止?
6    
7取决于代码是否能够响应取消
8

这也是为什么:

suspend 函数不是“自动可取消”,而是具体的挂起函数需要支持取消;普通同步代码则不会因为调用了 cancel() 就被强行打断。


Level 08:结构化并发,谁在等待谁?

难度: ⭐⭐⭐⭐⭐⭐

核心考点: coroutineScope 的生命周期与等待规则

1. 看代码

1fun main() = runBlocking {
2    coroutineScope {
3        launch {
4            delay(1000)
5            println("A")
6        }
7        launch {
8            delay(2000)
9            println("B")
10        }
11        println("C")
12    }
13    println("D")
14}
15

2. 先猜

D 会什么时候打印?

  • C 之后、A 之前
  • A 之后、B 之前
  • B 之后

3. 运行验证

1C
2A
3B
4D
5

4. 为什么?

进入:

1coroutineScope {
2    ...
3}
4

之后创建了两个子协程:

1coroutineScope
2├── launch  A
3└── launch  B
4

当前协程可以先执行:

1println("C")
2

coroutineScope 不会在子协程还没完成时就返回。

因此:

1C
2 
3等待 A
4 
5A
6 
7等待 B
8 
9B
10 
11coroutineScope 完成
12 
13D
14

所以:

1C
2A
3B
4D
5

这里就是结构化并发最核心的思想之一:

作用域不会在自己的子任务还没结束时就悄悄消失。


Level 09:作用域嵌套,任务如何结束?

难度: ⭐⭐⭐⭐⭐⭐⭐

核心考点: 嵌套作用域与执行流

1. 看代码

1fun main() = runBlocking {
2    launch {
3        delay(1000)
4        println("A")
5    }
6    coroutineScope {
7        launch {
8            delay(2000)
9            println("B")
10        }
11    }
12    println("C")
13}
14

2. 先猜

输出顺序是什么?

  • A → B → C
  • B → A → C
  • A → C → B

3. 运行验证

1A
2B
3C
4

4. 为什么?

这里有两个不同层级的任务。

第一个:

1launch {
2    delay(1000)
3    println("A")
4}
5

已经被创建出来,可以和后面的 coroutineScope 并行推进。

然后当前协程进入:

1coroutineScope {
2    launch {
3        delay(2000)
4        println("B")
5    }
6}
7

问题来了。

coroutineScope 会等待自己的子协程完成。

所以当前协程无法继续执行:

1println("C")
2

直到内部的 B 完成。

时间轴:

1T = 0
2A 开始
3B 开始
4
5T = 1000
6A
7
8T = 2000
9B
10
11coroutineScope 完成
12
13C
14

最终:

1A
2B
3C
4

这一关真正要理解的不是输出顺序,而是:

作用域不仅决定“任务属于谁”,还决定“当前执行流什么时候可以继续”。


Level 10:BOSS 战——协程生命周期综合实战

难度: ⭐⭐⭐⭐⭐⭐⭐⭐

核心考点: supervisorScope、手动取消、finally 与生命周期

1. 看代码

1fun main() = runBlocking {
2    supervisorScope {
3        val job1 = launch {
4            repeat(5) { i ->
5                println("Job 1: $i")
6                delay(200)
7            }
8        }
9        val job2 = launch {
10            try {
11                repeat(5) { i ->
12                    println("Job 2: $i")
13                    delay(200)
14                }
15            } finally {
16                println("Job 2 finally")
17            }
18        }
19        delay(300)
20        println("Cancelling Job 2...")
21        job2.cancel()
22    }
23    println("END")
24}
25

2. BOSS 战

一次回答下面 5 个问题:

  1. Job 1 会被 Job 2 的取消影响吗?
  2. Job 2 会打印几次?
  3. Job 2finally 会执行吗?
  4. Job 1 最终会打印到哪个 i
  5. END 什么时候打印?

3. 运行验证

通常可以看到类似:

1Job 1: 0
2Job 2: 0
3Job 1: 1
4Job 2: 1
5Cancelling Job 2...
6Job 2 finally
7Job 1: 2
8Job 1: 3
9Job 1: 4
10END
11

具体打印时序在边界时间点可能受到调度影响,但生命周期关系不会改变。


4. 为什么?

第一问:Job 1 会被取消吗?

不会。

这里是:

1job2.cancel()
2

取消的是 job2 自己。

并不是取消整个:

1supervisorScope
2

所以:

1Job 1 ───────────────→ 正常完成
2Job 2 ─────→ cancel  结束
3

第二问:Job 2 打印几次?

正常情况下:

1Job 2: 0
2Job 2: 1
3

300ms 左右执行:

1job2.cancel()
2

此时 job2 正处于下一次 delay() 或即将进入下一轮的过程中,因此取消后不会继续打印 2


第三问:finally 会执行吗?

会。

1try {
2    ...
3} finally {
4    println("Job 2 finally")
5}
6

取消导致协程结束时,正常的结构化清理代码仍然会进入 finally

所以:

1Job 2 finally
2

会被打印。

这也是为什么资源清理代码经常放在:

1finally
2

中。


第四问:Job 1 会执行到哪里?

Job 1 没有被取消。

所以它会继续:

1Job 1: 0
2Job 1: 1
3Job 1: 2
4Job 1: 3
5Job 1: 4
6

最终正常结束。


第五问:END 什么时候打印?

注意:

1supervisorScope {
2    ...
3}
4

同样是结构化作用域。

它不会因为 job2 被取消,就立刻结束。

它仍然需要等待自己的子协程完成。

因此:

1Job 2 被取消
2    
3Job 2 finally
4    
5Job 1 继续执行
6    
7Job 1 完成
8    
9supervisorScope 完成
10    
11END
12

最终:

1END
2

才会出现。


通关总结

走完这 10 关,实际上可以把 Kotlin 协程浓缩成三件事情。

1. 协程不是线程

协程是运行在线程之上的执行单元。

1协程
2  
3挂起 / 恢复
4  
5调度器
6  
7线程
8

所以:

1delay()
2

不代表切线程。

1withContext(Dispatchers.Default)
2

真正发生的是:

CoroutineContext 发生了变化,调度器可能因此选择不同的线程执行。


2. 取消是协作式的

调用:

1job.cancel()
2

本质上是:

告诉协程:你应该结束了。

但协程是否能够及时结束,取决于它是否能够响应取消。

挂起函数:

1delay()
2

通常可以响应取消。

而这种代码:

1while (true) {
2    doHeavyWork()
3}
4

如果没有检查:

1isActive
2

或者主动:

1yield()
2

那么即使调用:

1cancel()
2

也可能继续执行。

所以:

取消不是 kill,而是一种协作。


3. 结构化并发管理的是生命周期

这是最后也是最重要的一层。

launch 创建任务。

join 等待任务。

coroutineScope 管理子任务。

而这些机制最终都在解决一个问题:

这个任务到底属于谁?什么时候结束?谁负责等待它?

把整个闯关地图串起来:

1launch
2  
3创建任务
4
5delay / suspend
6  
7挂起与恢复
8
9join / await
10  
11等待任务
12
13withContext
14  
15切换执行上下文
16
17cancel
18  
19协作式结束
20
21coroutineScope
22  
23管理子任务生命周期
24
25supervisorScope
26  
27隔离子任务失败
28
29最终
30  
31结构化并发
32  
33让任务的生命周期有边界
34

Kotlin 协程闯关:看代码,猜结果》 是转载文章,点击查看原文


相关推荐


面试总被问云存储?块 / 对象 / 文件存储、快照备份一次性讲透
KIDULT°2026/9/12

华为云存储云服务 摘要:在云时代,存储是云业务的基石。日常我们接触 U 盘、移动硬盘、网盘都是存储设备,上云之后,华为云提供了四类主流存储服务:云硬盘 EVS、对象存储 OBS、弹性文件服务 SFS、云备份 CBR。本文带你搞懂各类存储的概念、特性、区别以及适用场景。 一、什么是存储云服务 存储是指将数据或信息保存在计算机或其他电子设备中的过程。存储可以是临时的,也可以是长期的,例如将文件保存在硬盘驱动器或云存储中。存储是计算机系统中非常重要的组成部分,它不但可以储存数据,还能通过存储实现容


hadoop高可用架构
kuroomi2026/9/4

hadoop高可用 IP 主机名 角色 192.168.150.172 server1 NameNode DFSZKFailoverController ResourceManager 192.168.150.173 server2 JournalNode QuorumPeerMain DataNode NodeManager


从“听得懂”到“干得了”:工业大模型落地工厂的三层进化路线
MobotStone2026/8/27

过去几年,大模型很火。但到了制造业,一个现实问题始终绕不开:会聊天的AI,真的能开机器、查故障、调参数、排产线吗? 答案是,单靠一个大模型很难。 工业现场和普通的聊天场景完全不同。一条生产线可能涉及设备说明书、工艺文件、传感器数据、机器视觉图像,以及MES、WMS、SCADA、PLC等各种系统。大模型不仅要“听得懂”,还要懂具体行业的专业知识,更重要的是,它最终得把判断变成实际动作。 因此,工业大模型并不是“一个模型打天下”,而更像一个分工明确的三层体系: 基础模型层:解决“懂不懂”,让AI拥


【AI智能体】Codex 生成高质量电商套图实战操作详解
小码农叔叔2026/8/19

目录 一、前言 二、Codex 介绍 2.1 Codex 是什么 2.2 Codex能做什么? 2.3 基于Codex 制作电商图片介绍 2.3.1 核心实现流程 2.3.2 实战操作建议 三、Codex 生成电商套图操作过程 3.1 前置准备 3.2 完整操作过程 3.2.1 规划设计方案 3.2.2 确定设计方案 3.2.3 根据产品主图规划设计方案 3.2.4 细节调整与完善 3.2.5 重新作图 3.2.6 封装成Skill 3.2.7 自定义Ski


如何在Windows环境选择适合自己的 AI Agent
Lei_official2026/8/6

背景 在使用 AI Agent 时,你是否曾经困惑过:应该选择什么环境、什么形态的 Agent 终端?以 Codex 为例,有 Desktop App,也有 CLI 工具。如果使用的是 Windows 环境,例如我,还面临着 PS7、WSL2 的选择。 根据奥卡姆剃刀原理,如非必要,勿增实体。对于功能相近的工具,我倾向于只保留最适合当前任务的一种。 尤其是 AI Agent 这类工具,使用的不只是工具本身,还有大量定制配置、Skill、MCP 等;这些内容也会不断更新、迭代。如果在同一台电脑上使


GitHub 热榜项目 - 周榜(2026-07-26)
CoderJia_2026/7/28

GitHub 热榜项目 - 周榜(2026-07-26) 生成于:2026-07-26 统计摘要 共发现热门项目: 22 个 Token赞助:siliconflow 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 本期热点趋势总结 本期 GitHub 热榜聚焦 AI Agent 工程化与开发者效率升级:代码审查图谱、 CLI / IDE 编排、 多模型路由与 token 压缩成为核心热点,配套的 Skil


一句话上线 AI Agent 应用:火山 Supabase + IGA Pages 全栈部署实践
火山引擎Agent社区2026/7/20

AI 全栈应用上线难在哪? 很多开发者在做全栈应用,尤其是 AI 应用时,真正耗时的地方往往不在业务代码本身。一个功能原型可能很快就能写出来:前端页面、登录注册、文件上传、数据库表、几段后端函数,再接一个大模型接口。但当它要从本地项目变成“别人能打开链接直接使用”的应用时,事情就会变复杂。 你需要准备数据库,执行建表脚本,配置行级权限,开通对象存储,部署后端函数,设置环境变量,再把前端打包上传。每一步都不算难,但串起来之后,部署流程很容易变成一次重复、繁琐、容易出错的基础设施工作。 火山引擎 S


AI图片工具到底有哪些?一份按能力维度整理的清单
怕浪猫2026/7/12

一、AI 图片生成类 产品核心优势网址Midjourney生成质量天花板,风格审美领先midjourney.comDALL·E 3与ChatGPT深度集成,理解能力强openai.com/dall-e-3Ideogram文字渲染能力最强,适合海报/Logoideogram.aiFlux新一代高质量模型,开源可部署flux1.aiStable Diffusion开源生态最强,可控性高stability.aiLeonar


油猴脚本创建webworker踩坑记录
天平2026/7/4

起因是我在使用vite-plugin-monkey编写油猴脚本,用ts编写webworker脚本,然后在一些网站创建webworker准备做一些耗时任务时,webworker一直没生效。我一直以为new Worker()梭哈就好了,没想到里面的门道这么多,整理了一些问题。 1.webworker不能直接使用非同源 假设谷歌有一个w.js脚本,在你的网站里面,不能直接new Worker('https://www.google.com/w.js')去加载。注意,这里是限制非同源,和跨域CORS没关


WorkBuddy 上手实战:打造一个可用的本地 AI 工作台
倔强的石头_2026/6/26

WorkBuddy 上手实战:打造一个可用的本地 AI 工作台 很多 AI 产品看上去都能聊天,但真正进到日常使用里,最常见的需求并不是闲聊,而是整理一段零散记录、起草一段通知、输出一份周报,或者把一个任务拆成清单。而WorkBuddy 更像一个本地工作台,而不是单一聊天框:它把任务输入、专家角色、技能扩展和自动化模板放在同一个界面里,适合把办公动作收拢到一处完成。 和只做对话的产品相比,WorkBuddy 的优势很明显: 任务入口更集中,不用在多个页面之间来回切换。 专家、技能、自动化是分层

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读