目录
什么是协程 (Coroutine)?
- 协程 = 一种可以 挂起 和 恢复 的轻量级线程。
- Kotlin 里,通过 launch {} / async {} 等构建器创建的,才是真正的 协程,是真正执行任务的实体。
- 每个协程都有一个 Job(生命周期控制器),用来追踪它的状态(Active、Cancelled、Completed)。
比如:
runBlocking {
launch {
println("我是一个协程")
}
}
这里的 launch { … } 就创建了一个 新协程。
什么是「协程作用域」?
协程作用域(CoroutineScope) = 协程的运行环境,用来管理协程的生命周期。
一个协程作用域必须要有一个 CoroutineContext,里面通常至少包含两个关键元素:
- Job:用于控制作用域内所有子协程的生命周期(取消/完成等)。
- Dispatcher:决定协程运行在哪个线程上。
换句话说:
👉 协程作用域就是「一片地盘」,在这片地盘上你可以创建协程,它们都归这个地盘的 Job 管。
在作用域里,你才能启动协程。
runBlocking { // runBlocking 创建了一个作用域
// 在这个作用域里才能 launch 协程
launch {
println("我运行在 runBlocking 的作用域里")
}
}
1. 协程作用域的来源
(1)直接实现 CoroutineScope 接口
比如你自己写一个类来持有作用域:
class MyScope: CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Default
}
(2)内置的作用域构造器
这些构造器会创建一个作用域,供你在代码块里启动协程:
-
coroutineScope {}- 挂起函数,创建一个普通作用域。
- 子协程失败会取消整个作用域。
-
supervisorScope {}- 挂起函数,创建一个监督作用域。
- 子协程失败不会影响兄弟协程。
(3)顶层作用域构建器(其实是协程启动器)
-
runBlocking {}- 会启动一个新的协程,并阻塞当前线程直到完成。
- 常用于 main() 或测试环境。
-
GlobalScope- 全局作用域,不依赖任何 Job → 协程和应用进程一样长命。
- 一般不推荐直接用。
总结:
- 「作用域」本身 = 一个 协程上下文 + 生命周期管理机制。
- coroutineScope / supervisorScope 并不是协程,而是挂起函数,在当前协程里建立一个新的子作用域。
- runBlocking / GlobalScope 是“顶层作用域”,一般用来作为整个应用或测试的入口。
- 没有作用域,就不能启动协程;作用域像“容器”,协程像“任务单元”。
什么是协程的上下文(CoroutineContext)
在 Kotlin 里,协程的上下文(CoroutineContext) 就像一个“运行环境”,
它保存了协程执行所需的一组元素,告诉协程 在哪里运行、叫什么名字、如何处理异常 等。
可以理解成:协程的“身份证 + 执行规则集合”。
CoroutineContext 是一个 元素集合(CoroutineContext.Element)。包含的元素有:
| 上下文元素 | 作用 | 示例 |
|---|---|---|
Job | 协程的生命周期,控制启动/取消/完成 | launch { ... } 会自动带一个 Job |
CoroutineDispatcher | 指定协程运行的线程(调度器) | Dispatchers.IO / Dispatchers.Default / Dispatchers.Main |
CoroutineName | 给协程起名字,方便调试和日志 | CoroutineName("Worker-1") |
CoroutineExceptionHandler | 统一处理未捕获异常 | CoroutineExceptionHandler { _, e -> ... } |
import kotlinx.coroutines.*
fun main(): kotlin.Unit = runBlocking {
launch(context = Dispatchers.IO + CoroutineName("MyCoroutine")) {
println("协程名字: ${coroutineContext[CoroutineName]}")
println("调度器: ${coroutineContext[CoroutineDispatcher]}")
println("Job: ${coroutineContext[Job]}")
}
}
输出类似:
协程名字: CoroutineName(MyCoroutine)
调度器: Dispatchers.IO
Job: StandaloneCoroutine{Active}@8dd9c13
总结:
- 协程上下文 = 协程的运行环境配置(由多个元素组成)。
- 常见元素有:Job(生命周期)、Dispatcher(线程调度)、Name(调试用)、ExceptionHandler(错误处理)。
- 可以通过 coroutineContext 访问当前协程的上下文。
- withContext 就是用来切换上下文的。
1. CoroutineContext 和 CoroutineScope的关系
协程上下文(CoroutineContext) 和 协程作用域(CoroutineScope) 关系很紧密,但不是同一个概念。
(1) 协程上下文(CoroutineContext)
-
定义:协程运行的环境配置,包含 Job、Dispatcher、CoroutineName、ExceptionHandler 等元素。
-
它是一个“配置集合”,不会独立存在,一般是 依附在协程或者作用域上。
-
可以用 coroutineContext 访问。
(2) 协程作用域(CoroutineScope)
定义:协程作用域就是一个接口,它持有一个 CoroutineContext,并提供 launch {}、async {} 等函数来启动协程。
- 所以 作用域 = 上下文 + 协程构建器。
- 每个作用域内部的子协程都会继承它的 CoroutineContext。
两者的关系
CoroutineScope 是对 CoroutineContext 的封装。
当你创建一个作用域时,你实际上就是给它配置了一个上下文。
如果在一个作用域里再 launch 子协程,子协程会继承父作用域的上下文, 例如:Dispatcher、CoroutineName
import kotlinx.coroutines.*
fun main(): kotlin.Unit = runBlocking {
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("MyScope"))
val job = scope.launch {
// 父协程
println("父协程 上下文对象:$coroutineContext, " +
"协程名字: ${coroutineContext[CoroutineName]}, " +
"调度器: ${coroutineContext[CoroutineDispatcher]}, " +
"Job: ${coroutineContext[Job]}")
// 子协程
launch {
println("子协程 上下文对象:$coroutineContext, " +
"协程名字: ${coroutineContext[CoroutineName]}, " +
"调度器: ${coroutineContext[CoroutineDispatcher]}, " +
"Job: ${coroutineContext[Job]}")
}
}
job.join()
}
输出结果:
父协程 上下文对象:[CoroutineName(MyScope), StandaloneCoroutine{Active}@982e489, Dispatchers.IO], 协程名字: CoroutineName(MyScope), 调度器: Dispatchers.IO, Job: StandaloneCoroutine{Active}@982e489
子协程 上下文对象:[CoroutineName(MyScope), StandaloneCoroutine{Active}@4af8a9f0, Dispatchers.IO], 协程名字: CoroutineName(MyScope), 调度器: Dispatchers.IO, Job: StandaloneCoroutine{Active}@4af8a9f0
可以看到每个 launch 都会创建新的 Job
-
Job 是协程上下文的一部分,代表协程的生命周期。
-
当你调用 launch,会自动生成一个新的 Job 并加到上下文里。
子协程会继承父协程的上下文,但会 替换 Job 元素,从而保证每个协程有自己独立的生命周期。
👉 所以 上下文不是“同一个对象”,但它们共享了大部分元素(比如 Dispatcher, CoroutineName)。
什么是Job?
在 Kotlin 协程中,Job 是协程的句柄(handle),代表一个可取消、可完成的后台任务。
你可以把它理解成 协程的生命周期管理器。
每次你用launch {}或 async {} 启动一个协程时,都会返回一个 Job(或者 Deferred<T>,它是 Job 的子类)。
Job 的主要功能
-
生命周期控制
- isActive → 协程是否正在运行
- isCompleted → 协程是否已经完成
- isCancelled → 协程是否被取消
-
取消协程
- job.cancel() → 发送取消信号
- job.cancelAndJoin() → 取消并等待协程完全退出
-
等待协程完成
- job.join() → 挂起当前协程,直到目标协程结束
-
层级关系
-
父协程的 Job 会管理所有子协程的 Job;
-
默认情况下,子协程失败会取消整个父 Job。
-
import kotlinx.coroutines.*
fun main() = runBlocking {
val job: Job = launch {
repeat(5) { i ->
println("协程执行 $i")
delay(300)
}
}
delay(700)
println("取消协程...")
job.cancelAndJoin() // 取消并等待退出
println("主协程结束")
}
可能输出:
协程执行 0
协程执行 1
协程执行 2
取消协程…
主协程结束
1. 父 Job 和 子 Job 的关系
在 Kotlin 协程里,每个 launch 或 async 启动的新协程都会 生成一个新的 Job。
-
这个 Job 会和启动它的协程的 Job 建立 父子关系。
-
因此,协程之间形成了一棵 Job 树(层级结构)。
父子 Job 的特性
(1) 生命周期管理
-
如果 父 Job 被取消,它的所有子 Job 也会被递归取消。
-
相反,子 Job 取消不会影响父 Job。
-
父协程要等所有子协程完成后,才会进入完成状态。
import kotlinx.coroutines.*
fun main(): Unit = runBlocking {
val parentJob = launch {
val childJob = launch {
try {
delay(1000)
println("子任务完成")
} finally {
println("子任务被取消")
}
}
delay(200)
println("取消父任务")
this.cancel() // 取消父任务
}
parentJob.join()
println("父任务结束")
}
结果:
取消父任务
子任务被取消
父任务结束
可以看到取消父job的时候,也会一起取消子job.
相反如果只是取消子job, 那么父job 还会继续运行, 例如:
import kotlinx.coroutines.*
fun main(): Unit = runBlocking {
val parentJob = launch {
val childJob = launch {
try {
delay(1000)
println("子任务完成")
} finally {
println("子任务被取消")
}
}
delay(200)
childJob.cancel() // 取消子job
delay(1200)
println("父任务正常执行")
}
parentJob.join()
println("父任务结束")
}
输出:
子任务被取消
父任务正常执行
父任务结束
join 和 cancelAndJoin
2. join() 函数的作用
作用:等待协程执行完成(无论正常完成还是异常结束)。
不会主动取消协程,只是挂起调用方,直到目标协程的生命周期结束。
👉 示例:
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("子协程完成")
}
println("等待子协程...")
job.join() // 仅仅等待
println("主协程继续")
}
输出结果:
等待子协程…
子协程完成
主协程继续
3. cancelAndJoin() 函数的作用
作用:先取消协程(发送取消信号),再等待它结束。
等价于:
job.cancel()
job.join()
常用于“干掉某个任务,然后等它完全退出”。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(5) {
println("子协程执行 $it")
delay(500) // 当job被cancel的时候,这里会抛出CancellationException异常,然后结束当前job
}
} finally {
println("子协程清理资源")
}
}
delay(1200) // 让子协程跑一会
println("取消子协程...")
job.cancelAndJoin()
println("主协程继续")
}
输出:
子协程执行 0
子协程执行 1
子协程执行 2
取消子协程…
子协程清理资源
主协程继续
什么是 Dispatchers?
在 Kotlin 协程中,Dispatchers 决定协程运行在哪个线程或线程池上。
它相当于协程的“调度器”(Scheduler),帮助我们指定任务的执行环境。
Kotlin 提供了几个常用的内置调度器:
| 调度器 | 说明 | 适用场景 |
|---|---|---|
Dispatchers.Default | 默认调度器,基于共享的后台线程池(CPU 核心数大小)。 | CPU 密集型任务(计算、排序、解析)。 |
Dispatchers.IO | 专门优化了 I/O 操作的线程池(比 Default 更大)。 | I/O 密集型任务(读写文件、网络请求、数据库操作)。 |
Dispatchers.Main | Android 或 UI 应用里的主线程调度器。 | 更新 UI(仅 Android/JavaFX/Compose 等支持)。 |
Dispatchers.Unconfined | 不指定具体线程,在哪启动就在哪运行,直到遇到挂起点后,恢复可能切到别的线程。 | 调试、测试用;少用于生产。 |
使用示例:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch(Dispatchers.Default) {
println("运行在 Default: ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
println("运行在 IO: ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
println("运行在 Unconfined: ${Thread.currentThread().name}")
delay(100)
println("恢复后可能在别的线程: ${Thread.currentThread().name}")
}
}
可能输出(不同机器略有不同):
运行在 Default: DefaultDispatcher-worker-1
运行在 IO: DefaultDispatcher-worker-2
运行在 Unconfined: main
恢复后可能在别的线程: kotlinx.coroutines.DefaultExecutor
1.自定义Dispatchers
在 Kotlin 协程里,内置的 Dispatchers.Default / IO / Main / Unconfined 已经覆盖了大多数场景,但有时候你可能需要更精细的调度,例如:
- 限制协程运行的线程数;
- 使用单线程执行某些任务;
- 和已有的线程池/Executor 对接。
这就需要 自定义 Dispatcher。
Dispatchers 本质是一个 CoroutineDispatcher。
我们可以通过 asCoroutineDispatcher() 方法,把一个 Executor 或 ExecutorService 转换为协程调度器。
val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
2. 固定大小线程池
import kotlinx.coroutines.*
import java.util.concurrent.*
fun main(): kotlin.Unit = runBlocking {
// 创建一个固定 2 个线程的线程池
val myDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
val jobs = List(5) { i ->
launch(myDispatcher) {
println("任务 $i 在线程: ${Thread.currentThread().name}")
delay(500)
}
}
jobs.joinAll()
// 注意:用完一定要关闭,否则程序不会退出
(myDispatcher.executor as ExecutorService).shutdown()
}
输出可能是:
任务 0 在线程: pool-1-thread-1
任务 1 在线程: pool-1-thread-2
任务 2 在线程: pool-1-thread-2
任务 3 在线程: pool-1-thread-1
任务 4 在线程: pool-1-thread-2
3. 单线程调度器(类似 Android Main)
import kotlinx.coroutines.*
import java.util.concurrent.*
val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun main(): Unit = runBlocking {
// 启动多个协程并发执行
val jobs = List(5) { i ->
launch(singleThreadDispatcher) {
println("任务 $i 在线程: ${Thread.currentThread().name}")
delay(200)
println("任务 $i 结束 在线程: ${Thread.currentThread().name}")
}
}
jobs.joinAll()
// 关闭 dispatcher,防止程序挂起
singleThreadDispatcher.close()
}
预期输出(不同机器线程名可能略有差别)
任务 0 在线程: pool-1-thread-1
任务 1 在线程: pool-1-thread-1
任务 2 在线程: pool-1-thread-1
任务 3 在线程: pool-1-thread-1
任务 4 在线程: pool-1-thread-1
任务 0 结束 在线程: pool-1-thread-1
任务 1 结束 在线程: pool-1-thread-1
任务 2 结束 在线程: pool-1-thread-1
任务 3 结束 在线程: pool-1-thread-1
任务 4 结束 在线程: pool-1-thread-1
说明:
- 所有协程都运行在 同一个线程:pool-1-thread-1;
- 尽管有多个协程并发执行,但调度器保证它们串行地使用同一个线程(即单线程语义);
如果你改成 Executors.newFixedThreadPool(2), 输出就会出现 pool-1-thread-1 和 pool-1-thread-2 两个不同线程。
任务 0 在线程: pool-1-thread-1
任务 1 在线程: pool-1-thread-2
任务 2 在线程: pool-1-thread-1
任务 3 在线程: pool-1-thread-1
任务 4 在线程: pool-1-thread-2
任务 1 结束 在线程: pool-1-thread-1
任务 0 结束 在线程: pool-1-thread-2
任务 2 结束 在线程: pool-1-thread-1
任务 3 结束 在线程: pool-1-thread-1
任务 4 结束 在线程: pool-1-thread-1
4. withContext
withContext 通常是和 Dispatchers 搭配使用的,因为它的主要作用就是 切换协程运行的上下文(Context),而上下文里最常见需要切换的就是 调度器(Dispatcher)。
suspend fun showData() {
val data = withContext(Dispatchers.IO) {
// 网络/数据库
"用户数据"
}
withContext(Dispatchers.Main) {
println("运行在主线程: ${Thread.currentThread().name}")
// 更新 UI
// textView.text = data
}
}
什么是结构化并发?
结构化并发的核心思想:
-
协程必须在某个 作用域(scope) 中运行;
-
子协程的生命周期受父协程作用域管理;
-
父协程不会结束,直到所有子协程都结束;
-
子协程异常会向上传递,默认会取消整个作用域。
-
这样可以避免“野生协程”泄漏(比如启动了后台协程却没人管,导致内存泄漏或异常丢失)。
coroutineScope 和 supervisorScope 都是结构化并发的一部分
很多人一开始会觉得:既然 supervisorScope 里的子协程不会互相传播异常,那是不是就“不结构化”了?其实不是。
它和 coroutineScope 一样,都会保证:
-
子协程都属于父作用域;
-
父作用域不会结束,直到子协程全部完成(不管是正常完成还是异常结束);
-
父作用域负责等待并管理子协程的生命周期。
只是 异常传播策略不同:
-
coroutineScope → 子协程异常会取消整个作用域。
-
supervisorScope → 子协程异常只会取消自己,不会影响兄弟协程,但父作用域仍然会等待所有子协程完成才结束。
什么是非结构化并发?
非结构化并发 = 启动的协程 没有被作用域管理,它的生命周期不受调用方控制。
这意味着:
-
协程可能比启动它的代码活得更久(内存泄漏风险)。
-
协程失败时,调用方可能感知不到异常。
-
协程的结束时机不可控,难以保证资源正确释放。
例如:
fun main() {
GlobalScope.launch { // 非结构化并发:脱离当前函数作用域
delay(1000)
println("子协程完成")
}
println("main函数结束")
}
运行结果很可能只输出:
main函数结束
因为 main 函数结束时,协程还没完成,进程就直接退出了。
这里的问题就是:协程的生命周期 和 main 函数完全脱节 → 这就是非结构化并发。
和结构化并发的对比
| 特点 | 非结构化并发 | 结构化并发 |
|---|---|---|
| 生命周期 | 脱离调用方作用域 | 嵌套在调用方作用域中 |
| 管理方式 | 协程“野生运行” | 由父作用域 Job 管理 |
| 安全性 | 容易泄漏,异常难追踪 | 安全可控,异常传播有规则 |
| 示例 | GlobalScope.launch {} | runBlocking + launch / coroutineScope / supervisorScope |
协程的异常的传播规则
在 Kotlin 协程中,子协程的异常是否会导致父协程结束,取决于你使用的协程构建器和作用域。默认情况下确实会传播异常,导致父协程结束。
1. 默认情况(launch / async + 普通 CoroutineScope)
如果在 结构化并发(例如 coroutineScope {} 或 launch {})中启动子协程:
子协程抛出未捕获的异常 → 异常会向上传递 → 会取消整个父协程作用域,从而结束父协程及其所有兄弟协程。
👉 举个例子:
import kotlinx.coroutines.*
fun main() = runBlocking {
println("最外层父协程开始")
launch {
println("里层父协程开始")
launch {
println("子协程1开始")
throw RuntimeException("子协程1异常")
}
launch {
delay(1000)
println("子协程2完成") // 永远不会执行
}
delay(1000)
println("里层父协程完成") // 永远不会执行
}
println("最外层父协程完成")
}
结果:
最外层父协程开始
最外层父协程完成
里层父协程开始
子协程1开始
Exception in thread “main” java.lang.RuntimeException: 子协程1异常
at MainKt$main$1$1KaTeX parse error: Expected group after '_' at position 371: …ines.BuildersKt_̲_BuildersKt.run…default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(Main.kt:22)
at MainKt.main(Main.kt)
子协程1异常导致整个父作用域取消 → 子协程2也被取消。
协程层级关系如下:
runBlocking (顶层父协程)
└── launch (里层父协程)
├── launch { 子协程1 }
└── launch { 子协程2 }
- 最外层 runBlocking 启动一个协程,挂在 main 线程。
- 里面第一个 launch { … } 是 父协程。
- 父协程里再启动两个子协程(子协程1、子协程2)。
异常传播机制(普通 launch):
默认规则:在一个普通作用域里,如果任意一个子协程失败,它会:
- 取消它的兄弟协程们(这里子协程2会被取消,来不及执行打印语句)。
- 取消父协程本身(所以父协程自己的 delay 和 println 也不会运行)。
- 因为父协程被取消,所以最终 子协程2完成 和 父协程完成 都不会输出。
2. 使用 supervisorScope
如果你希望子协程的异常不影响父协程和兄弟协程,需要使用 supervisorScope 或 SupervisorJob.
supervisorScope {} 是一个挂起函数,和 coroutineScope {} 类似,但作用域内的异常传播规则不同:
-
子协程失败只会取消自己;
-
兄弟协程继续运行;
-
父作用域会等待所有子协程结束后才返回。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("父协程开始")
supervisorScope {
println("父作用域开始")
launch {
println("子协程1开始")
throw RuntimeException("子协程1异常")
}
launch {
delay(1000)
println("子协程2完成") // 仍然会继续执行
}
delay(1000)
println("父作用域结束") // 仍然会继续执行
}
println("父协程结束")
}
结果:
父协程开始
父作用域开始
子协程1开始
Exception in thread “main” java.lang.RuntimeException: 子协程1异常
at MainKt$main$1$1KaTeX parse error: Expected group after '_' at position 371: …ines.BuildersKt_̲_BuildersKt.run…default(Builders.kt:48)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at MainKt.main(Main.kt:4)
at MainKt.main(Main.kt)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@326de728, BlockingEventLoop@25618e91]
父作用域结束
子协程2完成
父协程结束
这里的协程层级关系 解释一下
runBlocking { ... }
- 是最顶层的父协程。最外层,运行在 main 线程中。
- 会启动一个 最顶层的协程,并阻塞当前线程直到它内部的代码执行完。
supervisorScope { ... }
- 它不是新的协程,而是一个作用域构造器(CoroutineScope),继承了 runBlocking 的上下文(Job、Dispatcher 等),所以它挂靠在 runBlocking 的 Job 下。
- 它的生命周期受 runBlocking 管理,但它是 launch 子协程们的直接父作用域。
- 它为里面的 launch {} 提供了“父作用域”的角色。
- 只是一个“环境”,它本身不是协程,不会自己执行,只是用来管理上下文和子协程的生命周期。
- 它的父协程是runBlocking
launch { ... }(子协程1、子协程2)
- 这两个协程是 新的协程。
- 它们的 父作用域 是 supervisorScope。也会继承 coroutineScope 的上下文 → 其实最终还是挂在 runBlocking 的大 Job 树下
- 它们的 父协程 其实就是 runBlocking,因为 supervisorScope 本身并不是协程。
- 如果用了 supervisorScope,其中一个子协程失败(比如子协程1抛异常),不会影响到另一个(子协程2能继续执行)。
对应的关系层级如下:
runBlocking (顶层父协程)
│
└── supervisorScope (作用域,直接父作用域)
├── launch { 子协程1 }
└── launch { 子协程2 }
3. 使用SupervisorJob
SupervisorJob 是一种特殊的 Job,不会将子协程的失败传播给父作用域。
子协程失败只会取消自己,兄弟协程和父作用域都不受影响。
import kotlinx.coroutines.*
fun main() = runBlocking {
// 父作用域带 SupervisorJob
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
scope.launch {
println("子协程1开始")
delay(500)
throw RuntimeException("子协程1异常")
}
scope.launch {
println("子协程2完成") // 正常执行
}
scope.launch {
try {
delay(3000)
println("子协程3完成") // 会被scope.cancel 取消
} catch (e: CancellationException) {
println("子协程3被取消")
}
}
delay(1500) // 等待子协程完成
println("父协程依然存活: ${scope.isActive}")
scope.cancel() // 最后手动取消父作用域
}
结果:
子协程1开始
子协程2完成
Exception in thread “DefaultDispatcher-worker-3” java.lang.RuntimeException: 子协程1异常
at MainKt$main$1 1. i n v o k e S u s p e n d ( M a i n . k t : 31 ) a t k o t l i n . c o r o u t i n e s . j v m . i n t e r n a l . B a s e C o n t i n u a t i o n I m p l . r e s u m e W i t h ( C o n t i n u a t i o n I m p l . k t : 33 ) a t k o t l i n x . c o r o u t i n e s . D i s p a t c h e d T a s k . r u n ( D i s p a t c h e d T a s k . k t : 100 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r . r u n S a f e l y ( C o r o u t i n e S c h e d u l e r . k t : 586 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r 1.invokeSuspend(Main.kt:31) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586) at kotlinx.coroutines.scheduling.CoroutineScheduler 1.invokeSuspend(Main.kt:31)atkotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)atkotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)atkotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)atkotlinx.coroutines.scheduling.CoroutineSchedulerWorker.executeTask(CoroutineScheduler.kt:829)
at kotlinx.coroutines.scheduling.CoroutineScheduler W o r k e r . r u n W o r k e r ( C o r o u t i n e S c h e d u l e r . k t : 717 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r Worker.runWorker(CoroutineScheduler.kt:717) at kotlinx.coroutines.scheduling.CoroutineScheduler Worker.runWorker(CoroutineScheduler.kt:717)atkotlinx.coroutines.scheduling.CoroutineSchedulerWorker.run(CoroutineScheduler.kt:704)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@4ccdae72, Dispatchers.Default]
父协程依然存活: true
子协程3被取消
可以看到兄弟协程2和父协程仍然继续执行, 同时scope.cancel会让正在等待(delay)的子协程抛出CancellationException异常, 导致兄弟协程3结束.
什么是挂起函数(suspend)
编译器把 suspend 函数编译成可中断/可恢复的状态机;在挂起点(如 delay/withContext/任意 suspend)释放线程,稍后在同或不同线程恢复。
要点:
-
只能在协程或其它 suspend 里调用
-
不阻塞线程,但能“阻塞”协程的执行流程(挂起)
- 阻塞 (block):线程停在那里干等,不能干别的
- 挂起 (suspend):协程让出线程,调度器可以在这段时间调度别的协程继续执行
要“证明”协程是挂起而不是阻塞,可以让多个协程并发运行,并观察它们共享同一个线程,且同时完成。
import kotlinx.coroutines.*
fun main() = runBlocking {
println("start @ ${Thread.currentThread().name}")
// 启动两个协程在同一个线程
launch {
delay(5000) // 挂起,不占用线程
println("Task1 done @ ${Thread.currentThread().name}")
}
launch {
delay(5000) // 同样挂起
println("Task2 done @ ${Thread.currentThread().name}")
}
println("Launched tasks @ ${Thread.currentThread().name}")
}
输出:
start @ main
Launched tasks @ main
Task1 done @ main
Task2 done @ main
👉 两个协程在 同一个线程 (main) 执行,且都能在 5 秒后完成。如果 delay 真的是阻塞,它会卡死线程,可以看到前2条日志是先打印的, 所以并不会阻塞线程(main)的执行。 而后面2条日志几乎是同一时刻打印的, 彼此并不会互相等待, 他们是并发执行的.
CoroutineExceptionHandler
CoroutineExceptionHandler 是 Kotlin 协程(Coroutines)中的一个异常处理机制,用于处理 未捕获的协程异常。它的作用和使用场景可以总结如下:
(1) 基本作用
在 Kotlin 协程中,如果一个协程抛出异常,而异常没有被 try-catch 捕获,通常会导致协程所在的 CoroutineScope 的父协程或者 SupervisorJob 被取消。CoroutineExceptionHandler 可以:
-
捕获协程中未处理的异常
-
进行日志记录、上报或者自定义处理
-
防止程序直接崩溃(在一些情况下)
⚠️ 注意:它只处理 根协程(顶层协程)中的异常,不会捕获通过 async 返回的异常(需要通过 await 显式捕获)。
所谓顶层协程(Top-level coroutine)是指:直接在某个 CoroutineScope 下启动,没有父协程(或者父协程不会处理异常)的协程。
- 异常在顶层协程中抛出时,如果没有捕获,就会通过 CoroutineExceptionHandler 处理。
- GlobalScope.launch 是典型的顶层协程。
- 异常无法向上层传播(没有父协程),所以会被 CoroutineExceptionHandler 捕获。
import kotlinx.coroutines.*
fun main() = runBlocking {
// 创建一个异常处理器
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
// launch 协程,挂载异常处理器
val job = GlobalScope.launch(context = handler) {
println("Throwing exception from launch")
throw AssertionError("Oops!")
}
job.join()
println("Done")
}
输出结果:
Throwing exception from launch
Caught java.lang.AssertionError: Oops!
Done
关键点
- 只捕获未处理的异常
- 如果协程内部已经用 try-catch 捕获,CoroutineExceptionHandler 不会被调用。
- 对 launch 有效,对 async 无效
import kotlinx.coroutines.*
fun main() = runBlocking {
// 创建一个异常处理器
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val deferred = GlobalScope.async(handler) {
throw AssertionError("Fail in async")
}
deferred.await() // handler 不会捕获异常 这里还是会抛出异常
println("Done")
}
结果:
Exception in thread “main” java.lang.AssertionError: Fail in async
at MainKt$main 1 1 1deferred 1. i n v o k e S u s p e n d ( M a i n . k t : 76 ) a t k o t l i n . c o r o u t i n e s . j v m . i n t e r n a l . B a s e C o n t i n u a t i o n I m p l . r e s u m e W i t h ( C o n t i n u a t i o n I m p l . k t : 33 ) a t k o t l i n x . c o r o u t i n e s . D i s p a t c h e d T a s k . r u n ( D i s p a t c h e d T a s k . k t : 100 ) a t k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r . r u n S a f e l y ( C o r o u t i n e S c h e d u l e r . k t : 586 ) a t > k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r 1.invokeSuspend(Main.kt:76) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586) at >kotlinx.coroutines.scheduling.CoroutineScheduler 1.invokeSuspend(Main.kt:76)atkotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)atkotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)atkotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:586)at>kotlinx.coroutines.scheduling.CoroutineSchedulerWorker.executeTask(CoroutineScheduler.kt>:829)
at >kotlinx.coroutines.scheduling.CoroutineScheduler W o r k e r . r u n W o r k e r ( C o r o u t i n e S c h e d u l e r . k t : 7 > 17 ) a t > k o t l i n x . c o r o u t i n e s . s c h e d u l i n g . C o r o u t i n e S c h e d u l e r Worker.runWorker(CoroutineScheduler.kt:7>17) at >kotlinx.coroutines.scheduling.CoroutineScheduler Worker.runWorker(CoroutineScheduler.kt:7>17)at>kotlinx.coroutines.scheduling.CoroutineSchedulerWorker.run(CoroutineScheduler.kt:704)
协程的取消机制
1. 协作取消
Kotlin 协程是 协作式取消 的:协程本身不会被强制停止,而是通过 挂起点检查取消状态 来响应取消。
主要相关 API:
-
Job.cancel():请求取消协程。
-
Job.cancelAndJoin():请求取消并等待协程完成。
-
CoroutineScope.isActive:检查协程是否还活跃。
-
withContext(NonCancellable):在取消的情况下仍然执行关键操作。
所谓协作式就是协程的取消需要协作取消, 它并不是立即硬性停止。
- 当你调用 job.cancel() 或者 coroutineScope.cancel() 时,它只会 发送取消信号,标记协程为“已取消”。
- 协程本身仍然会继续执行,直到遇到检查取消状态的地方如:
- delay()、yield()、withContext() 等挂起函数,它们会自动抛出 CancellationException
- 或者你手动调用 coroutineContext.ensureActive()
也就是说,如果你的协程里没有挂起点,也没有调用 ensureActive(),协程是不会立刻停止的。
在 Kotlin 协程中,协作式取消依赖 挂起点(suspending point)来检查协程是否被取消。也就是说,只有遇到这些挂起点,协程才会响应取消。常见的协作取消挂起点 API 列表:
| API / 函数 | 说明 |
|---|---|
delay(timeMillis) | 延迟指定时间,遇到取消会抛出 CancellationException |
yield() | 暂停当前协程,给其他协程机会运行,同时检查取消 |
withContext(context) { ... } | 切换上下文时会检查取消状态 |
await() (Deferred) | 等待异步任务结果,如果协程被取消会抛出异常 |
join() / joinAll() | 等待协程完成,同时响应取消 |
select { ... } | 等待多个挂起函数完成,协程取消时会终止选择 |
Channel 的 receive() / send() | 通道操作是挂起点,协程被取消会立即抛异常 |
Flow.collect { ... } | 收集 Flow 元素时挂起,会响应取消 |
Mutex.withLock { ... } | 获取锁是挂起点,挂起期间会响应取消 |
Semaphore.acquire() | 获取许可时挂起,会响应取消 |
withTimeout / withTimeoutOrNull | 超时挂起,会响应取消 |
suspendCancellableCoroutine { ... } | 自定义挂起函数时,如果使用该 API,会自动支持协作取消 |
2. NonCancellable
NonCancellable 是一个 特殊的 CoroutineContext。
用途:在协程取消后仍然保证某些关键操作执行(如清理资源、写日志、提交事务)。
典型场景:
-
finally 块中的清理操作
-
在取消状态下执行 IO 或数据库操作
NonCancellable 只能用于少量关键操作,不宜滥用,否则会阻塞取消逻辑。
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
repeat(5) { i ->
println("Working $i ...")
delay(500)
}
} finally {
// 即使协程被取消,这里也会执行
withContext(NonCancellable) {
println("Cleaning up...")
delay(1000) // 模拟清理耗时操作
println("Cleanup done")
}
}
}
delay(1100)
println("Cancelling job")
job.cancelAndJoin()
println("Main done")
}
输出:
Working 0 …
Working 1 …
Working 2 …
Cancelling job
Cleaning up…
Cleanup done
Main done
说明:
- finally 内的代码会在协程取消后执行。
- 使用 withContext(NonCancellable) 可以保证 即使取消,关键逻辑仍会完成。
协程并发控制
1. async/await
说明:
-
async 用于启动并发协程,并返回 Deferred 对象。
-
await() 获取结果,遇到异常会抛出。
-
适用于多个任务并行执行后合并结果的场景。
-
与 launch 不同,async 可以返回结果。
特点:
-
异步并发执行,提高性能。
-
异常需要在 await() 时捕获。
import kotlinx.coroutines.*
suspend fun api1(): Int { delay(200); return 1 }
suspend fun api2(): Int { delay(300); return 2 }
fun main() = runBlocking {
val a = async { api1() }
val b = async { api2() }
println("sum = ${a.await() + b.await()}") // sum = 3
}
2. Mutex(协程友好锁)
说明:
-
Mutex 是协程专用的互斥锁。
-
withLock { … } 会挂起当前协程,直到获得锁。
-
避免传统锁阻塞线程,更适合协程并发场景。
适用场景:
- 多协程同时修改共享资源时保证线程安全。
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val mutex = Mutex()
var counter = 0
coroutineScope {
repeat(1_000) {
launch(Dispatchers.Default) {
repeat(100) {
mutex.withLock { counter++ }
}
}
}
}
println("counter = $counter") // 100_000
}
3. Semaphore(限流 / 限并发)
说明:
-
Semaphore 控制同时运行的协程数量(限流)。
-
permits 表示同时允许的协程数量。
-
acquire() 获取许可,release() 释放许可。
适用场景:
限制对某些资源的并发访问,例如网络请求、数据库连接池。
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
val sem = Semaphore(permits = 4) // 定义最大并发4个任务
val tasks = (1..10).map { i ->
launch(Dispatchers.Default) {
sem.acquire() // 每次开始任务就占用一个Semaphore
try {
println("run $i @ ${Thread.currentThread().name}")
delay(200)
} finally {
sem.release() // 每次调用一次就释放一个Semaphore, 否则4个都占用完就会停在sem.acquire()了
}
}
}
tasks.joinAll()
}
输出结果:
run 1 @ DefaultDispatcher-worker-1
run 2 @ DefaultDispatcher-worker-2
run 3 @ DefaultDispatcher-worker-3
run 4 @ DefaultDispatcher-worker-4
run 7 @ DefaultDispatcher-worker-5
run 5 @ DefaultDispatcher-worker-7
run 6 @ DefaultDispatcher-worker-6
run 8 @ DefaultDispatcher-worker-8
run 9 @ DefaultDispatcher-worker-6
run 10 @ DefaultDispatcher-worker-1
4. 原子变量(轻量计数)
说明:
-
AtomicInteger 提供原子操作,线程安全。
-
incrementAndGet() 保证计数操作不会丢失。
-
比 Mutex 更轻量,适合简单计数器。
适用场景:
-
高并发下的计数、累加、标记操作。
-
不需要复杂锁定逻辑时使用。
来看一个错误的并发操作:
import kotlinx.coroutines.*
fun main() = runBlocking {
var counter = 0
coroutineScope {
repeat(1_000) {
launch(Dispatchers.Default) {
repeat(100) { counter++ }
}
}
}
println("counter = $counter")
}
输出结果:
counter = 96813
结果和预想的100000 不符合, 正确的做法如下:
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.*
fun main() = runBlocking {
val c = AtomicInteger(0)
coroutineScope {
repeat(1_000) {
launch(Dispatchers.Default) {
repeat(100) { c.incrementAndGet() }
}
}
}
println("counter = ${c.get()}") // 100000
}
AtomicInteger 的常用方法表格,包含方法、功能说明和返回值类型:
| 方法 | 功能说明 | 返回值 |
|---|---|---|
get() | 获取当前值 | 当前整数值 |
set(newValue: Int) | 设置新值 | Unit |
lazySet(newValue: Int) | 延迟写入新值,对性能优化有用 | Unit |
getAndSet(newValue: Int) | 获取当前值,然后设置为新值 | 原值 |
incrementAndGet() | 自增 1,并返回自增后的值 | 新值 |
getAndIncrement() | 获取当前值,然后自增 1 | 原值 |
decrementAndGet() | 自减 1,并返回自减后的值 | 新值 |
getAndDecrement() | 获取当前值,然后自减 1 | 原值 |
addAndGet(delta: Int) | 增加指定值,并返回新值 | 新值 |
getAndAdd(delta: Int) | 获取当前值,然后增加指定值 | 原值 |
compareAndSet(expect: Int, update: Int) | 如果当前值等于期望值 expect,则更新为 update | Boolean(是否成功) |
getAndUpdate(updateFunction: (Int) -> Int) | 根据函数更新值,返回原值 | 原值 |
updateAndGet(updateFunction: (Int) -> Int) | 根据函数更新值,返回新值 | 新值 |
getAndAccumulate(x: Int, accumulator: (Int, Int) -> Int) | 使用二元函数更新值,返回原值 | 原值 |
accumulateAndGet(x: Int, accumulator: (Int, Int) -> Int) | 使用二元函数更新值,返回新值 | 新值 |
5. limitedParallelism
limitedParallelism(n) 可以限制 同一个 CoroutineDispatcher/CoroutineScope 下协程的最大并发数为 n。
它返回一个新的 CoroutineDispatcher,在这个 dispatcher 上启动的协程同时最多只会有 n 个在运行,其余协程会挂起等待。
简单说,它是一种 轻量级的限流工具,不需要手动 acquire/release。
import kotlinx.coroutines.*
fun main() = runBlocking {
// 创建一个限制并行度为 3 的调度器
val limitedDispatcher = Dispatchers.Default.limitedParallelism(3)
val jobs = (1..10).map { i ->
launch(limitedDispatcher) {
println("Start task $i @ ${Thread.currentThread().name}")
delay(1500) // 模拟耗时操作
println("End task $i @ ${Thread.currentThread().name}")
}
}
jobs.joinAll()
}
说明:
虽然我们启动了 10 个协程,但同一时间最多只有 3 个协程在执行。
不需要手动管理 Semaphore,limitedParallelism 自动挂起等待。
Channel(协程间直接通信)
Kotlin 协程中的 Channel,它是协程间直接通信(CSP 风格)的核心工具。
-
Channel 类似 线程安全的队列,用于 协程间传递数据。
-
一个协程可以发送数据到 Channel,另一个协程可以接收数据。
-
基于 CSP(Communicating Sequential Processes)模型,协程通过消息传递协作,而不是共享状态。
1. CSP 的核心思想
-
进程(Process)
-
每个进程是独立的、顺序执行的任务单元。
-
进程内部可以做计算、处理数据,但不直接访问其他进程的状态。
-
-
通信(Communication)
-
进程之间不共享内存,而是通过 消息传递 进行交互。
-
消息传递通常是 同步 或 挂起式:发送方和接收方在通信时协调完成。
-
-
顺序执行(Sequential)
-
每个进程内部是顺序执行的,但多个进程并行运行。
-
并发是通过进程间的通信来协调,而不是共享变量和锁。
-
2. Kotlin 中的 CSP
Kotlin 协程的 Channel 就是 CSP 的体现:
-
协程 = 进程
-
send() / receive() = 消息传递
-
挂起点 = 进程在等待通信时暂停执行
特点:
-
无共享状态:协程通过 Channel 传递数据,而不是访问同一变量。
-
挂起式同步:发送方和接收方在必要时挂起,避免忙等待。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main(): kotlin.Unit = runBlocking {
val channel = Channel<Int>(capacity = 5) // 带缓冲区
// 生产者
launch {
for (i in 1..5) {
println("Send $i")
channel.send(i)
}
channel.close() // 关闭 Channel
}
// 消费者
launch {
delay(3000)
for (y in channel) { // 使用迭代器自动接收直到关闭
println("Received $y")
delay(300) // 模拟处理耗时
}
}
}
输出结果:
Send 1
Send 2
Send 3
Send 4
Send 5
Received 1
Received 2
Received 3
Received 4
Received 5
-
生产者和消费者各自顺序执行自己的逻辑。
-
通过 Channel 通信交换数据,而不共享任何变量。
-
这种模式就是 CSP 风格:顺序进程 + 消息传递。
channel.close() 的作用
-
close() 并 不会丢掉已经发送到 Channel 的元素。
-
它主要作用是 告诉接收方没有更多数据了,以及 阻止发送方继续发送。
换句话说:
-
发送端 send() 之后的数据在缓冲区里仍然可被接收。
-
接收端可以通过迭代(for (x in channel))或 receive() 继续取数据,直到缓冲区为空 且 Channel 被关闭。
-
一旦关闭,后续 send() 会抛异常 ClosedSendChannelException。
3. Channel 类型
(1) 按角色分类(接口维度)
| 类型 | 说明 | 常用方法 | 使用场景 |
|---|---|---|---|
| SendChannel | 只能发送数据的接口 | send(), trySend() | 生产者(只写,不读) |
| ReceiveChannel | 只能接收数据的接口 | receive(), tryReceive(), consumeEach {} | 消费者(只读,不写) |
| Channel | 同时继承了 SendChannel<T> 和 ReceiveChannel<T> | 既能 send() 也能 receive() | 通用场景 |
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
@OptIn(ExperimentalCoroutinesApi::class)
fun CoroutineScope.producer(): ReceiveChannel<Int> = produce {
repeat(5) {
send(it)
}
}
fun main() = runBlocking {
val numbers: ReceiveChannel<Int> = producer()
for (x in numbers) {
println(x)
}
}
上面在 main 里只能 receive,不能 send,保证接口清晰。
(2) 按实现方式分类(容量/策略维度)
| 类型 | 容量 | 行为特征 | 使用场景 |
|---|---|---|---|
| Rendezvous Channel(默认) | 0 | 发送方挂起直到接收方 receive() | 严格同步,一对一通信 |
| Buffered Channel | N(固定大小) | 缓冲区满时 send() 挂起 | 生产/消费速率不匹配 |
| Unlimited Channel | ∞ | send() 永不挂起,可能内存暴涨 | 高吞吐量,允许堆积 |
| Conflated Channel | 1(仅保留最新值) | 旧值会被覆盖,只保留最新 | 状态流、UI 渲染 |
| Actor Channel | N(由实现决定) | 封装状态 + 顺序处理消息 | 并发安全的共享状态 |
(1) Rendezvous Channel(容量 = 0, 默认)
如果不使用缓冲, 那么发送端每send一个数据, 接收端就会收到一个数据, 如果没有接收端, 那么发送端就会一直挂起在send函数, 例如:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main(): kotlin.Unit = runBlocking {
val channel = Channel<Int>() // 无缓冲区
// 生产者
launch {
for (i in 1..5) {
println("Send $i")
channel.send(i) // 会挂起,直到有接收者
}
channel.close() // 关闭 Channel
}
// 消费者
launch {
delay(5000) // 延迟接收
for (y in channel) { // 使用迭代器自动接收直到关闭
println("Received $y")
delay(300) // 模拟处理耗时
}
}
}
输出结果:
Send 1 // 打印这句后会挂起, 直到5秒后, 后面的日志才输出, 而且是每收到一个,才发送下一个
Received 1
Send 2
Received 2
Send 3
Received 3
Send 4
Received 4
Send 5
Received 5
(2) Buffered Channel(容量 = N)
有缓冲,发送可以先放进缓冲区,直到满了才挂起。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main(): kotlin.Unit = runBlocking {
val channel = Channel<Int>(capacity = 2) // 缓冲区大小 2
launch {
repeat(3) {
println("before Send $it")
channel.send(it) // 第 0,1 放入缓冲区不会挂起,第 2 会挂起
println("after Sent $it ✅")
}
channel.close() // 发送完后记得关闭channel
}
launch {
delay(5000) // 接收晚点开始
repeat(3) {
val value = channel.receive()
println("Received $value ✅")
}
}
}
输出结果:
before Send 0
after Sent 0 ✅
before Send 1
after Sent 1 ✅
before Send 2 // 打印这行的时候会挂起, 直到5秒后, 后面的内容才输出
Received 0 ✅
Received 1 ✅
Received 2 ✅
after Sent 2 ✅
(3) Unlimited Channel(容量无限大)
所有 send() 都不会挂起,但可能内存爆掉。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main(): kotlin.Unit = runBlocking {
val channel = Channel<Int>(Channel.UNLIMITED)
launch {
repeat(5) {
println("Send $it")
channel.send(it) // 永远不会挂起
}
channel.close() // 记得关闭channel
}
launch {
delay(2000) // 消费很慢
for (x in channel) {
println("Received $x")
}
}
}
输出结果:
Send 0
Send 1
Send 2
Send 3
Send 4 // 发送端全部事件都发送完, 并停留在此, 等待2秒后, 接收端消费后, 才开始打印下面的日志
Received 0
Received 1
Received 2
Received 3
Received 4
(4) Conflated Channel(容量 = 1, 只保留最新值)
丢弃旧消息,只保留最新。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main(): kotlin.Unit = runBlocking {
val channel = Channel<Int>(Channel.CONFLATED)
launch {
repeat(5) {
println("Send $it")
channel.send(it) // 旧的值会被覆盖,只保留最新
}
channel.close()
}
launch {
delay(2000) // 消费很慢
for (x in channel) {
println("Received $x") // 只会拿到最后一个
}
}
}
输出结果:
Send 0
Send 1
Send 2
Send 3
Send 4 // 发送端全部发送完后, 挂起在此, 等待2秒后, 接收端只会收到最后一次发送的值
Received 4
4.Channel 基本 API
| 分类 | 方法 | 功能 |
|---|---|---|
| 通用 | close(cause: Throwable?) | 关闭通道,之后不能再发送,接收方还能消费剩余数据 |
isClosedForSend | 是否已关闭发送端 | |
isClosedForReceive | 是否已关闭接收端 | |
| 发送端(SendChannel) | send(element) | 挂起发送,缓冲满会挂起 |
trySend(element)(替代 offer) | 尝试发送,立即返回 ChannelResult | |
| 接收端(ReceiveChannel) | receive() | 挂起接收,缓冲空会挂起 |
tryReceive()(替代 poll) | 尝试接收,立即返回 ChannelResult | |
receiveCatching() | 安全接收,即使关闭或异常也不会抛出,返回 ChannelResult | |
iterator() / for (x in channel) | 迭代消费数据,直到通道关闭 | |
| 扩展函数 | consumeEach { ... } | 顺序消费所有元素,简化写法 |
consumeAsFlow() / receiveAsFlow() | 把 ReceiveChannel 转换为 Flow,方便与 Flow API 一起用 | |
toList() / toSet() | 一次性收集所有元素到集合(需要通道关闭) |
Channel 的挂起点行为
-
发送挂起点:send() 当缓冲区满时挂起。
-
接收挂起点:receive() 当缓冲区为空时挂起。
-
取消安全:挂起时协程被取消会抛 CancellationException。
注意点
- Channel 是协作式通信
- 发送和接收都是挂起操作,协程需要通过挂起点合作。
- 关闭 Channel
- 生产者完成后要 close(),否则消费者可能无限挂起。
- 关闭 Channel 不会丢失已经发送的消息
- 消费者仍能接收到缓冲区里剩余的数据
- 关闭 Channel 只会影响 发送端:禁止继续 send(),会抛异常
- 接收端通过迭代或 receive 可以继续消费,直到缓冲区空 + Channel 关闭
- 缓冲区选择
- Rendezvous(无缓冲):严格同步,发送和接收必须匹配。
- 缓冲型:允许一定程度的生产超前消费。
- CONFLATED:只保留最新值,适合状态更新场景。
- 性能
- Channel 有一定开销,如果只做简单的计数器或状态传递,可以考虑 AtomicInteger 或 SharedFlow。
(1) receiveAsFlow
👉 扩展在 ReceiveChannel 上
- 作用:把一个 ReceiveChannel 转换成一个 Flow。
- 特点:是 非终结性 的,调用后你仍然可以继续使用这个 ReceiveChannel。
- receiveAsFlow() 本质就是把 channel 的接收迭代器 转换成 Flow。
- 它不会改变顺序,默认情况下你收到的就是 发送的顺序。
- 多个收集器(collect)去收的时候,它们会 竞争同一个通道 的数据。数据被分摊,导致不是每个收集器都能拿到完整数据。
- 使用场景:你想用 Flow 操作符(map、filter、collect 等)来消费通道里的数据。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
fun main(): kotlin.Unit = runBlocking {
val channel = Channel<Int>()
launch {
repeat(5) {
delay(100)
channel.send(it)
}
channel.close() // 关闭通道
}
val flow = channel.receiveAsFlow()
launch { flow.collect { println("Collector #1 got $it") } }
launch { flow.collect { println("Collector #2 got $it") } }
launch { flow.collect { println("Collector #3 got $it") } }
}
输出结果:
Collector #1 got 0
Collector #2 got 1
Collector #3 got 2
Collector #1 got 3
Collector #2 got 4
(2) consumeAsFlow
👉 扩展在 ReceiveChannel 上
- 作用:同样把通道转换成 Flow,但会在消费结束后自动取消(关闭)通道。
- 特点:是 终结性 的,一旦调用,你就不能再用这个 ReceiveChannel。
- 它会把 ReceiveChannel 消费掉(消费 = 只能用一次)。
- 一旦这个 Flow 被收集过,通道就关闭/失效了, 再次collect就会抛出异常了。
- 使用场景:你想一次性把通道内容流式处理掉,不再继续使用通道。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val channel = Channel<Int>()
// 生产者
launch {
repeat(5) {
channel.send(it)
}
channel.close()
}
// 使用 consumeAsFlow 消费,并自动关闭 channel
channel.consumeAsFlow()
.collect { println("Got $it") }
// channel 已经被消费完,不能再使用
println("Channel is closed: ${channel.isClosedForReceive}")
}
输出结果:
Got 0
Got 1
Got 2
Got 3
Got 4
Channel is closed: true
5. Actor 函数
在 Kotlin Coroutines 里,actor {} 确实是一个 函数(构建器),它不是像 Swift 那样的 actor 关键字。
actor 是 kotlinx.coroutines.channels 包里的一个 扩展函数, 函数定义如下:
@ObsoleteCoroutinesApi
public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
): SendChannel<E> {
val newContext = newCoroutineContext(context)
val channel = Channel<E>(capacity)
val coroutine = if (start.isLazy)
LazyActorCoroutine(newContext, channel, block) else
ActorCoroutine(newContext, channel, active = true)
if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)
coroutine.start(start, coroutine, block)
return coroutine
}
它需要一个 CoroutineScope来启动一个协程,返回值是一个 SendChannel<E>,你可以往这个 channel 里发送消息(事件),而 Actor 内部会顺序处理这些消息。
查看源码可知通过ActorScope可以得到channel对象
@ObsoleteCoroutinesApi
public interface ActorScope<E> : CoroutineScope, ReceiveChannel<E> {
/**
* A reference to the mailbox channel that this coroutine [receives][receive] messages from.
* It is provided for convenience, so that the code in the coroutine can refer
* to the channel as `channel` as apposed to `this`.
* All the [ReceiveChannel] functions on this interface delegate to
* the channel instance returned by this function.
*/
public val channel: Channel<E>
}
Actor = 协程 + Channel
- 协程:在后台运行,循环处理消息
- Channel:外部通过发送消息与 Actor 交互
- ActorScope:提供对 channel 的访问
- 返回的SendChannel必须手动调用 close() 来关闭通道,否则 actor 协程会一直挂起等待消息。
Kotlin 的 Actor 既可以用来做协程之间的通信,也可以用来保证并发安全。
1. 协程之间的通信
Actor 的底层就是 Channel,而 Channel 本身就是 协程之间的消息传递机制。
-
一个协程(Producer)可以 send 消息到 Actor
-
Actor 协程会顺序 receive 消息并处理
所以从这个角度,Actor = 特殊的消息消费者,它让不同协程之间通过消息交互,而不用共享变量。
2. 保证并发安全
因为 Actor 协程内部是 串行处理消息 的,消息队列是 FIFO:
-
即使多个协程同时调用 send(),消息会被排队
-
Actor 内部一次只处理一个消息
-
不需要加锁,就能保证共享状态的安全访问
👉 所以 Actor 模型其实就是:让共享状态和逻辑都封装在一个协程里,外部只能通过消息和它交互。
示例:通信 + 并发安全
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
// 定义消息类型 (Actor 只能通过消息通信)
sealed class CounterMsg
// 递增消息
object Increment : CounterMsg()
// 查询当前值的消息,带一个 "回应信封"
// CompletableDeferred 相当于 "一个可写一次的 Future"
// 发送者可以等待它,Actor 收到后填入值
class GetValue(val response: CompletableDeferred<Int>) : CounterMsg()
@OptIn(ObsoleteCoroutinesApi::class)
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // Actor 内部状态(只能 Actor 自己访问)
// 消息循环,逐个顺序处理
for (msg in channel) {
when (msg) {
is Increment -> counter++ // 收到递增指令
is GetValue -> msg.response.complete(counter)
// 收到查询指令,把结果写进 response
// 这样调用方可以拿到 counter 的值
}
}
}
fun main(): kotlin.Unit = runBlocking {
val counter: SendChannel<CounterMsg> = counterActor()
// 并发发送 1000 个递增消息
repeat(1000) {
counter.send(Increment)
}
// 构造一个 CompletableDeferred 等待返回值
val response = CompletableDeferred<Int>()
// 发送 "查询消息",并把 response 附带过去
counter.send(GetValue(response))
// 挂起等待 Actor 回复(Actor 在 GetValue 时调用 complete)
val value = response.await()
println("Final count: $value") // 1000
counter.close() // 使用完后, 必须手动关闭通道!!!
}
什么是Actor Isolation
所谓Isolation 就是隔离的意思, Actor函数内部的状态(state)只能被 Actor 自己的代码访问和修改,外部只能通过消息(send/await)与它交互。
- 每个 Actor 就像一个“小隔间”或“小黑屋”,里面的状态对外是隔离的。
- 外部协程、线程、任务 不能直接访问 Actor 内部的状态。
- 外部要想改变 Actor 的状态,必须 发送消息,由 Actor 自己处理。
👉 这样就避免了传统并发编程中的 共享内存冲突 和 加锁问题。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
sealed class CounterMsg
object Increment : CounterMsg()
class GetValue(val response: CompletableDeferred<Int>) : CounterMsg()
@OptIn(ObsoleteCoroutinesApi::class)
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // Actor 内部状态(隔离)
for (msg in channel) {
when (msg) {
is Increment -> counter++ // 只有 Actor 协程自己能改
is GetValue -> msg.response.complete(counter) // 返回结果
}
}
}
fun main() = runBlocking {
val counter = counterActor()
// 并发修改,但不会有线程安全问题
repeat(1000) { counter.send(Increment) }
val response = CompletableDeferred<Int>()
counter.send(GetValue(response))
println("Final count: ${response.await()}") // 输出 1000
counter.close()
}
6. producer 函数
producer 函数 其实和 actor 属于同一类东西,都是 kotlinx.coroutines 提供的 构建协程 + 通道的便捷函数。
它本质上是个 CoroutineScope 的扩展函数,返回一个 ReceiveChannel,用来表示“一个会不断生产数据的协程”。源码定义如下:
@ExperimentalCoroutinesApi
public fun <E> CoroutineScope.produce(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = Channel.RENDEZVOUS,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> =
produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block)
-
返回值:ReceiveChannel(只能收数据的通道)。
-
内部:会启动一个协程,把 block 里的逻辑放进去执行, 作为生产者实现数据的发送, 当生产者协程 执行结束时,通道会自动关闭。
-
作用:专门用于生产者协程,负责往通道里发送数据, 然后得到返回值的地方去接收数据。
对比 actor
-
actor → 返回 SendChannel(别人给它发消息,它顺序处理 → 常用来做状态机/并发安全)。
-
produce → 返回 ReceiveChannel(它自己不断发数据,别人来消费 → 常用来做生产者)。
可以说:
-
actor 用于收消息并处理。
-
producer 用于发消息供别人消费。
(1) producer 构造生产者
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runBlocking {
// 启动生产者
val numbers: ReceiveChannel<Int> = produce {
for (i in 1..5) {
delay(500)
println("Send $i")
send(i) // 向通道发送数据
}
}
// 消费者
delay(2000) // 延迟2秒后再消费
for (x in numbers) {
println("Received $x")
}
println("Done!")
}
输出结果:
Send 1 // 打印这行之后会挂起, 2秒后消费者开始接收, 每收到一个, 才会发送下一个
Received 1
Send 2
Received 2
Send 3
Received 3
Send 4
Received 4
Send 5
Received 5
Done!
(2) producer 结合 actor 使用
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
@OptIn(ExperimentalCoroutinesApi::class, ObsoleteCoroutinesApi::class)
fun main(): kotlin.Unit = runBlocking {
// 生产者:产生数字
val producer: ReceiveChannel<Int> = produce {
for (i in 1..10) {
send(i)
}
}
// 消费者 Actor:计算平方
val squareActor: SendChannel<Int> = actor {
for (num in channel) {
println("Square of $num = ${num * num}")
}
}
// 把生产者的数据发给 Actor
for (num in producer) {
squareActor.send(num)
}
producer.cancel()
squareActor.close() // 关闭发送端
}
输出结果:
Square of 1 = 1
Square of 2 = 4
Square of 3 = 9
Square of 4 = 16
Square of 5 = 25
Square of 6 = 36
Square of 7 = 49
Square of 8 = 64
Square of 9 = 81
Square of 10 = 100
总结:
- producer {} → 返回 ReceiveChannel,用于“生产者协程”。
- actor {} → 返回 SendChannel,用于“消息处理协程”。
- 它们都是 CoroutineScope 的扩展函数,必须在一个 scope 内使用。
👉 所以 produce 和 actor 是 对 Channel 的模式化封装,目的就是:
-
produce = 简化生产者协程 + 自动关闭。
-
actor = 简化消费者协程 + 保证并发安全。
632

被折叠的 条评论
为什么被折叠?



