Kotlin 协程中的热数据源与冷数据源
导言
数据源通常分为热数据源和冷数据源。热数据源会主动产生数据,生产过程通常独立于消费者;冷数据源只有在消费者真正需要数据时才开始执行。
本文整理 List、Sequence、Channel 和 Flow 的执行模型,并说明如何根据数据来源、是否需要挂起以及是否需要协程取消来选择合适的类型。
一、热数据源与冷数据源
热数据源
热数据源通常具有以下特点:
- 创建或启动后就可能立即执行;
- 不需要等待消费者请求;
- 可以独立于消费者产生元素;
- 可能保存已经产生的元素;
- 多次使用时通常不需要重新计算。
常见的热数据源包括 List、Set、Channel、StateFlow 和 SharedFlow。
冷数据源
冷数据源通常具有以下特点:
- 惰性执行;
- 只有真正需要元素时才开始计算;
- 通常不保存中间结果;
- 只执行必要的操作;
- 可以表示无限数据;
- 每次使用时可能重新执行。
常见的冷数据源包括 Sequence、Java Stream、普通 Kotlin Flow 和 RxJava 数据流。
| 热数据源 | 冷数据源 |
|---|---|
List、Set |
Sequence |
Channel |
Java Stream |
StateFlow、SharedFlow |
普通 Flow |
这只是通常情况,并不是绝对规则。例如,buffer 和 channelFlow 会让 Flow 的上游提前执行,而 SharedFlow 和 StateFlow 本身就是热流。
二、List 是立即计算的数据集合
List 表示一个已经计算完成或已经存在的集合。调用方通常需要等待所有元素准备完成后,才能得到这个列表。
kotlin
fun getList(): List<String> =
List(3) { index ->
Thread.sleep(1_000)
"User$index"
}
fun main() {
val list = getList()
println("Function started")
list.forEach {
println(it)
}
}
输出:
text
(约 3 秒后)
Function started
User0
User1
User2
List.map 也会立即执行:
kotlin
val result = listOf(1, 2, 3).map {
println("Processing $it")
it * 2
}
调用 map 时,所有元素都已经被处理。
三、Sequence 是惰性数据源
如果元素可以逐个计算,并且希望生成一个元素后立即使用它,可以使用 Sequence。
kotlin
fun getSequence(): Sequence<String> =
sequence {
repeat(3) { index ->
Thread.sleep(1_000)
yield("User$index")
}
}
fun main() {
val sequence = getSequence()
println("Function started")
sequence.forEach {
println(it)
}
}
输出:
text
Function started
(约 1 秒)
User0
(约 1 秒)
User1
(约 1 秒)
User2
Sequence 创建时不会执行:
kotlin
val sequence = sequenceOf(1, 2, 3)
.map {
println("Processing $it")
it * 2
}
println("Sequence created")
此时只会打印 Sequence created。调用终止操作后才会真正计算:
kotlin
val first = sequence.first()
println(first)
输出:
text
Processing 1
2
first() 只需要第一个元素,因此后面的元素不会被处理。
中间操作与终止操作
中间操作只描述计算规则,例如:
text
map、filter、take、drop、sortedBy
终止操作才会触发计算,例如:
text
first()、find { ... }、toList()、count()、sum()、forEach { ... }
可以理解为:
text
中间操作:描述如何计算
终止操作:真正执行计算
Sequence 可以表示无限数据
kotlin
val numbers = generateSequence(1) {
it + 1
}
println(numbers.take(5).toList())
输出:
text
[1, 2, 3, 4, 5]
Sequence 也适合读取文件:
kotlin
import java.io.File
fun countCharactersInFile(path: String): Int =
File(path).useLines { lines ->
lines.sumOf { line -> line.length }
}
四、List 与 Sequence 的处理顺序
kotlin
fun mapValue(value: Int): Int {
print("m$value ")
return value * value
}
fun filterValue(value: Int): Boolean {
print("f$value ")
return value >= 10
}
List 会先完成全部 map,再执行 find:
kotlin
listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.map { mapValue(it) }
.find { filterValue(it) }
输出:
text
m1 m2 m3 m4 m5 m6 m7 m8 m9 m10 f1 f4 f9 f16 16
Sequence 会对每个元素完整执行处理链:
kotlin
sequenceOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.map { mapValue(it) }
.find { filterValue(it) }
输出:
text
m1 f1 m2 f4 m3 f9 m4 f16 16
找到 16 后就停止,因此不会处理后面的元素。
五、Sequence 不能直接调用挂起函数
Sequence 的终止操作是普通函数,Sequence.iterator() 也不是挂起函数。因此不能在 Sequence 构建器中直接调用 delay 或其他普通挂起函数:
kotlin
fun getSequence(): Sequence<String> =
sequence {
repeat(3) {
delay(1_000) // 编译错误
yield("User$it")
}
}
原因可以表示为:
text
挂起函数 delay()
↓
Sequence 的普通 iterator()
↓
无法挂起协程
因此,下面这种分页网络请求不应该使用 Sequence:
kotlin
fun allUsersSequence(api: UserApi): Sequence<User> =
sequence {
var page = 0
do {
// takePage 是挂起函数,无法在 sequence 中调用
val users = api.takePage(page++)
yieldAll(users)
} while (!users.isNullOrEmpty())
}
六、Sequence 可能阻塞其他协程
kotlin
fun getSequence(): Sequence<String> =
sequence {
repeat(3) { index ->
Thread.sleep(1_000)
yield("User$index")
}
}
如果在单线程调度器中消费这个 Sequence:
kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
suspend fun main() {
newSingleThreadContext("main").use { dispatcher ->
withContext(dispatcher) {
launch {
repeat(3) {
delay(100)
println("Processing on coroutine")
}
}
getSequence().forEach {
println(it)
}
}
}
}
Thread.sleep(1_000) 阻塞了唯一线程,因此另一个协程必须等 Sequence 消费完成后才能运行。
这说明:
Sequence 的惰性不等于非阻塞,也不等于协程友好。
七、Flow 介绍
Flow 表示一个异步计算得到的值流。它的核心成员函数是挂起函数 collect:
kotlin
interface Flow<out T> {
suspend fun collect(
collector: FlowCollector<T>
)
}
collect 类似集合的 forEach,但它可以在等待数据时挂起当前协程,而不会阻塞线程。
kotlin
flow.collect { value ->
println(value)
}
如果数据生成过程需要调用挂起函数,应使用 Flow:
kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
fun getFlow(): Flow<String> =
flow {
repeat(3) { index ->
delay(1_000)
emit("User$index")
}
}
Flow 的消费操作也是挂起友好的:
kotlin
suspend fun main() {
getFlow().collect { value ->
println(value)
}
}
八、Flow 是冷数据源
创建 Flow 时,生产逻辑不会立即执行:
kotlin
fun users(): Flow<String> = flow {
println("Flow started")
for (id in 1..3) {
delay(500)
emit("User$id")
}
}
val userFlow = users()
println("Flow created")
只有调用 collect 后,才会打印 Flow started 并开始生成数据:
kotlin
userFlow.collect { user ->
println(user)
}
每次收集都会重新执行生产代码:
kotlin
val flow = flow {
println("Producing data")
emit(1)
emit(2)
}
flow.collect {
println("First: $it")
}
flow.collect {
println("Second: $it")
}
输出:
text
Producing data
First: 1
First: 2
Producing data
Second: 1
Second: 2
flow { ... } 只是描述生产规则;真正执行发生在 collect、first、find 或 toList 等终止操作中。
九、Channel 与 Flow 的生命周期差异
Channel:立即启动生产协程
kotlin
private fun CoroutineScope.makeChannel() = produce {
println("Channel started")
for (i in 1..3) {
delay(1_000)
send(i)
}
}
produce 是 CoroutineScope 的扩展函数,因为它会立即启动一个新的生产协程。
kotlin
suspend fun main(): Unit = coroutineScope {
val channel = makeChannel()
delay(1_000)
println("Calling channel...")
for (value in channel) {
println(value)
}
println("Consuming again...")
for (value in channel) {
println(value)
}
}
第二次消费没有输出,因为 Channel 中的元素通常只会被接收一次:
text
生产者 ──发送 1──> Channel ──接收──> 消费者
↓
元素已经被取走
Channel 默认容量通常为 0,也就是 Rendezvous Channel。send 必须等接收者准备好后才能完成:
text
produce 启动
↓
计算数据
↓
send(value)
↓
没有接收者,send 挂起
↓
接收者出现
↓
send 完成
Flow:收集时才开始生产
kotlin
private fun makeFlow() = flow {
println("Flow started")
for (i in 1..3) {
delay(1_000)
emit(i)
}
}
suspend fun main(): Unit = coroutineScope {
val flow = makeFlow()
delay(1_000)
println("Calling flow...")
flow.collect { value ->
println(value)
}
println("Consuming again...")
flow.collect { value ->
println(value)
}
}
每次 collect 都会从头开始执行 Flow,因此两次收集都会重新输出 Flow started 和 1、2、3。
十、使用 Flow 进行分页请求
Flow 适合产生通过网络分页获取的数据:
kotlin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
fun allUsersFlow(api: UserApi): Flow<User> =
flow {
var page = 0
do {
val users = api.takePage(page++) ?: emptyList()
emitAll(users)
} while (users.isNotEmpty())
}
emitAll(users) 会把当前页面的用户逐个发出。
只获取第一个用户:
kotlin
val firstUser = allUsersFlow(api).first()
通常只需要请求第一页,找到第一个用户后就会停止收集。
获取全部用户:
kotlin
val allUsers = allUsersFlow(api).toList()
这会持续请求下一页,直到遇到空页面。
查找指定用户:
kotlin
val user = allUsersFlow(api)
.find { it.id == targetId }
这会持续获取分页数据,直到找到目标用户或所有页面都处理完。
十一、Flow 支持取消和结构化并发
kotlin
suspend fun loadUsers(api: UserApi): List<User> =
allUsersFlow(api).toList()
如果调用它的父协程被取消,Flow 的收集过程也会被取消:
kotlin
val job = launch {
allUsersFlow(api).collect { user ->
println(user)
}
}
delay(1_000)
job.cancel()
取消后,后续页面请求不应该继续进行。这也是 Flow 比普通 Sequence 更适合网络和数据库数据流的重要原因。
十二、如何选择数据源
| 需求 | 推荐类型 |
|---|---|
| 所有数据已经准备好,并且需要重复读取 | List 或 Set |
| 同步、惰性、按需计算 | Sequence |
| 多个协程之间传递任务或事件 | Channel |
| 网络、数据库、分页和异步计算 | Flow |
| 保存并共享最新状态 | StateFlow |
| 广播事件给多个订阅者 | SharedFlow |
最终可以这样记忆:
text
List:已经准备好的数据
Sequence:同步的惰性计算
Channel:协程之间的通信通道
Flow:支持挂起和取消的异步数据流
总结
热数据源是生产者主动生产数据,消费者从中接收;冷数据源则由消费者触发生产。
Sequence 适合按需计算,但它的消费操作是同步的,可能阻塞线程;Flow 专门为协程设计,支持挂起、取消、结构化并发和异步数据流。
在 Kotlin 协程中,一般可以遵循以下原则:
text
完整数据已经准备好
→ List
需要同步惰性计算,不能调用挂起函数
→ Sequence
需要协程间任务通信
→ Channel
需要网络、数据库、分页、异步计算或协程取消
→ Flow