Kotlin 协程五
wptr33 2024-12-14 15:32 47 浏览
上一节主要介绍了协程的同步、异步、上下文、调度。通过前面的介绍,其实主要都是基于挂起函数,然后挂起函数是异步返回单个值,那如何返回多个异步计算的值呢?这就是本节主要要讲述的Kotlin Flows。
Flow之前
在介绍Flow之前,我们看一下还可以通过哪些方式来产生多个值?
集合(collections)
在Kotlin中可以使用集合来表示多个值。例如,我们可以有一个简单的函数,返回一个包含三个数字的列表,然后使用forEach将它们全部打印出来:
fun simple(): List<Int> = listOf(1, 2, 3)
fun main() {
simple().forEach { value -> println(value) }
}执行结果:
1
2
3系列(Sequences)
使用阻塞代码来计算这些数字(每次计算需要100毫秒),可以使用一个序列来表示这些数字:
fun simple(): Sequence<Int> = sequence {
for (i in 1..3) {
Thread.sleep(100)
yield(i)
}
}
fun main() {
simple().forEach { value -> println(value) }
}执行结果:
1
2
3挂起方法
通过上面这种计算会阻塞正在运行代码的主线程。当这些值由异步代码计算时,我们还可以用挂起修饰符标记简单函数,这样它就可以在不阻塞的情况下执行工作,并以列表的形式返回结果:
suspend fun simple(): List<Int> {
delay(100)
return listOf(1, 2, 3)
}
fun main() = runBlocking<Unit> {
simple().forEach { value -> println(value) }
}执行结果:
1
2
3Flows
使用List<Int>结果类型,意味着我们只能一次返回所有值。为了表示正在异步计算的值流,我们可以使用Flow<Int>类型,就像上面使用Sequence<Int>类型用于同步计算值一样:
fun simple(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}
fun main() = runBlocking<Unit> {
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
}
simple().collect { value -> println(value) }
}执行结果
I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3来看一下Flow代码有哪些不同之处:
- Flow类型的构建器函数称为flow;
- flow{…}中的代码可以挂起;
- simple函数不再用suspend修饰符标记;
- 使用emit函数从流发射值;
- 使用collect函数从流中收集值。
冷流
Flow流是类似于序列的冷流——流构建器中的代码直到流被收集后才运行。我们可以通过下面的例子更清晰的看出来:
fun simple(): Flow<Int> = flow {
println("Flow started")
for (i in 1..3) {
delay(100)
emit(i)
}
}
fun main() = runBlocking<Unit> {
println("Calling simple function...")
val flow = simple()
println("Calling collect...")
flow.collect { value -> println(value) }
println("Calling collect again...")
flow.collect { value -> println(value) }
}执行结果:
Calling simple function...
Calling collect...
Flow started
1
2
3
Calling collect again...
Flow started
1
2
3这就是simple函数(返回流)没有使用挂起修饰符标记的关键原因。simple()调用本身快速返回,不等待任何东西。每次收集时,流都会重新开始,这就是为什么每次调用collect时我们都会看到“流已启动”的原因。
构建Flow
通过flow{…} 来构建Flow是最基本的一种方式。接下来,我们看看还可以通过哪些方式可以构建Flow:
- flowOf构建器定义了一个流,它发出一组固定的值。
- 可以使用. asflow()扩展函数将各种集合和序列转换为流。
(1..3).asFlow().collect { value -> println(value) }Flow操作
Flow流可以使用操作符进行转换,就像转换集合和序列一样。中间操作符应用于上游流并返回下游流。这些操作符也是冷操作,就像Flow一样。意味着对这样一个操作符的调用本身并不是一个挂起函数,它会快速执行并返回一个新的转换流。
基本操作符,如map和filter。这些操作符与序列的一个重要区别是,这些操作符中的代码块可以调用挂起函数。
map
suspend fun performRequest(request: Int): String {
delay(1000)
return "response $request"
}
fun main() = runBlocking<Unit> {
(1..3).asFlow()
.map { request -> performRequest(request) }
.collect { response -> println(response) }
}执行结果:
response 1
response 2
response 3transform
它可以用来模仿简单的转换,如map和filter,以及实现更复杂的转换。使用转换操作符,我们可以发射任意次数的任意值。
例如下面这段代码,使用transform我们可以在执行一个长时间运行的异步请求之前发出一个字符串,并在它后面跟着一个响应:
fun main() = runBlocking<Unit> {
(1..3).asFlow()
.transform { request ->
emit("Making request $request")
emit(performRequest(request))
}
.collect { response -> println(response) }
}执行结果
Making request 1
response 1
Making request 2
response 2
Making request 3
response 3take
take操作符会在达到相应限制时取消流的执行。协程中的取消总是通过抛出异常来执行,这样所有的资源管理函数(比如try{…}finally{…} )在取消的情况下正常运行:
fun numbers(): Flow<Int> = flow {
try {
emit(1)
emit(2)
println("This line will not execute")
emit(3)
} finally {
println("Finally in numbers")
}
}
fun main() = runBlocking<Unit> {
numbers()
.take(2) // take only the first two
.collect { value -> println(value) }
} 执行结果:
1
2
Finally in numbers通过结果可以看出,只取出了前两个值,然后就取消了协程。
Flow是按顺序执行的
Flow流执行规则如下:
- 流的每个单独收集都是按顺序执行的,除非使用了操作多个流的特殊操作符。
- 集合直接在调用终止操作符的协程中工作。
- 默认情况下不会启动新的协程。
- 每个发出的值都由上游到下游的所有中间操作符处理,然后传递给终端操作符。
通过下面的例子,过滤偶数并将它们映射到字符串:
fun main() = runBlocking<Unit> {
(1..5).asFlow()
.filter {
println("Filter $it")
it % 2 == 0
}
.map {
println("Map $it")
"string $it"
}.collect {
println("Collect $it")
}
}执行结果:
Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5通过上面的结果,我们可以更直白得看出流的顺序执行规则。
Flow上下文
Flow流的collect操作总是发生在调用协程的上下文中。例如,存在一个simple流,并在指定的上下文中运行,而不管simple流的实现细节:
withContext(context) {
simple().collect { value ->
println(value)
}
}默认情况下,flow{…}构建器运行在相应流的收集器提供的上下文中。例如:
fun simple(): Flow<Int> = flow {
log("Started simple flow")
for (i in 1..3) {
emit(i)
}
}
fun main() = runBlocking<Unit> {
simple().collect { value -> log("Collected $value") }
} 执行结果:
[main @coroutine#1] Started simple flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3simple().collect是从主线程调用,简单的流体也在主线程中调用。
注意:使用withContext的一个陷阱
我们开发过程,对于长时间运行且消耗cpu代码,可能需要在Dispatchers.Default上下文中执行,然后ui更新代码可能需要在Dispatchers.Main上下文中执行。通常Kotlin协程使用withContext更改代码中的上下文,但是flow{…}构建器必须遵守上下文一致性,不允许从不同的上下文emit。
fun simple(): Flow<Int> = flow {
// 试图通过Dispatchers.Default改变上下文,以执行耗时操作
kotlinx.coroutines.withContext(Dispatchers.Default) {
for (i in 1..3) {
Thread.sleep(100)
emit(i)
}
}
}
fun main() = runBlocking<Unit> {
simple().collect { value -> println(value) }
} 执行结果:
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323],
but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default].
Please refer to 'flow' documentation or use 'flowOn' instead
at ...flowOn
上面的异常是指出必须使用flowOn函数来改变流发射上下文。下面的例子显示了更改流上下文的正确方法,它还打印了相应线程的名称,以显示它是如何工作的:
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun simple(): Flow<Int> = flow {
for (i in 1..3) {
Thread.sleep(100)
log("Emitting $i")
emit(i)
}
}.flowOn(Dispatchers.Default)
fun main() = runBlocking<Unit> {
simple().collect { value ->
log("Collected $value")
}
} 执行结果:
[DefaultDispatcher-worker-1] Emitting 1
[main] Collected 1
[DefaultDispatcher-worker-1] Emitting 2
[main] Collected 2
[DefaultDispatcher-worker-1] Emitting 3
[main] Collected 3通过执行结果可以看出,flow{…}在后台线程中工作,而在主线程中进行收集。
这里要注意,flowOn操作符改变了流的默认顺序性质。现在收集发生在一个协程(“coroutine#1”)中,而发射发生在另一个协程(“coroutine#2”)中,该协程与收集的协程同时运行在另一个线程中。当上游流必须在其上下文中更改CoroutineDispatcher时,flowOn操作符为其创建一个新的协程。
总结
如果我们之前已经熟悉响应式流或响应式框架(如RxJava和Reactor项目)的人来说,Flow的设计可能看起来非常熟悉。事实上,它的设计灵感来自于Reactive Streams及其各种实现。但是Flow的主要目标是拥有尽可能简单的设计,对Kotlin和suspend友好,并使用结构化并发。
Flow是一个响应式流,可以将其转换为响应式,反之亦然。由kotlinx.coroutines提供转换器:
- kotlinx-coroutines-reactive for Reactive Streams,
- kotlinx-coroutines-reactor for Reactor
- kotlinx-coroutines-rx2/kotlinx-coroutines-rx3 for RxJava2/RxJava3
相关推荐
- oracle数据导入导出_oracle数据导入导出工具
-
关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...
- 继续学习Python中的while true/break语句
-
上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...
- python continue和break的区别_python中break语句和continue语句的区别
-
python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...
- 简单学Python——关键字6——break和continue
-
Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...
- 2-1,0基础学Python之 break退出循环、 continue继续循环 多重循
-
用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...
- Python 中 break 和 continue 傻傻分不清
-
大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...
- python中的流程控制语句:continue、break 和 return使用方法
-
Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...
- L017:continue和break - 教程文案
-
continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...
- 作为前端开发者,你都经历过怎样的面试?
-
已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...
- 面试被问 const 是否不可变?这样回答才显功底
-
作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...
- 2023金九银十必看前端面试题!2w字精品!
-
导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...
- 前端面试总结_前端面试题整理
-
记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...
- 由浅入深,66条JavaScript面试知识点(七)
-
作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...
- 2024前端面试真题之—VUE篇_前端面试题vue2020及答案
-
添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...
- 今年最常见的前端面试题,你会做几道?
-
在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...
- 一周热门
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)
