Kotlin 协程二 —— 通道 Channel
一、 Channel 基本使用
1.1 Channel 的概念
Channel 翻译过来为通道或者管道,实际上就是个队列, 是一个面向多协程之间数据传输的 BlockQueue,用于协程间通信。Channel 允许我们在不同的协程间传递数据。形象点说就是不同的协程可以往同一个管道里面写入数据或者读取数据。它是一个和 BlockingQueue 非常相似的概念。区别在于:BlockingQueue 使用 put 和 take 往队列里面写入和读取数据,这两个方法是阻塞的。而 Channel 使用 send 和 receive 两个方法往管道里面写入和读取数据。这两个方法是非阻塞的挂起函数,鉴于此,Channel 的 send 和 receive 方法也只能在协程中使用。
1.2 Channel 的简单使用
val channel = Channel<Int>()
launch {
// 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送
for (x in 1..5) channel.send(x * x)
}
// 这里我们打印了 5 次被接收的整数:
repeat(5) { println(channel.receive()) }
println("Done!")
输出结果:
1
4
9
16
25
Done!
1.3 Channel 的迭代
如果要取出 Channel 中所有的数据,可以使用迭代。
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (x in 1..5) {
channel.send(x * x)
}
}
val iterator = channel.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
println(next)
}
println("Done!")
}
可以简化成:
val channel = Channel<Int>()
launch {
// 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送
for (x in 1..5) channel.send(x * x)
}
for (y in channel) {
println(y)
}
println("Done!")
此时输出结果:
1
4
9
16
25
最后一行 Done! 没有打印出来,并且程序没有结束。此时,我们发现,这种方式,实际上是我们一直在等待读取 Channel 中的数据,只要有数据到了,就会被读取到。
1.4 close 关闭 Channel
我们可以使用 close() 方法关闭 Channel,来表明没有更多的元素将会进入通道。
val channel = Channel<Int>()
launch {
// 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送
for (x in 1..5) channel.send(x * x)
channle.close() // 结束发送
}
for (y in channel) {
println(y)
}
println("Done!")
从概念上来讲,调用 close 方法就像向通道发送了一个特殊的关闭指令,这个迭代停止,说明关闭指令已经被接收了。所以这里能够保证所有先前发送出去的原色都能在通道关闭前被接收到。
对于一个 Channel,如果我们调用了它的 close,它会立即停止接受新元素,也就是说这时候它的 isClosedForSend 会立即返回 true,而由于 Channel 缓冲区的存在,这时候可能还有一些元素没有被处理完,所以要等所有的元素都被读取之后 isClosedForReceive 才会返回 true。
输出结果:
1
4
9
16
25
Done!
1.5 Channel 是热流
Flow 是冷流,只有调用末端流操作的时候,上游才会发射数据,与 Flow 不同,Channel 是热流,不管有没有订阅者,上游都会发射数据。
二、Channel 的类型
2.1 SendChannel 和 ReceiveChannel
Channel 是一个接口,它继承了 SendChannel 和 ReceiveChannel 两个接口
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E>
SendChannel
SendChannel 提供了发射数据的功能,有如下重点接口:
- send 是一个挂起函数,将指定的元素发送到此通道,在该通道的缓冲区已满或不存在时挂起调用者。如果通道已经关闭,调用发送时会抛出异常。
- trySend 如果不违反其容量限制,则立即将指定元素添加到此通道,并返回成功结果。否则,返回失败或关闭的结果。
- close 关闭通道。
- isClosedForSend 判断通道是否已经关闭,如果关闭,调用 send 会引发异常。
ReceiveChannel
ReceiveChannel 提供了接收数据的功能,有如下重点接口:
- receive 如果此通道不为空,则从中检索并删除元素;如果通道为空,则挂起调用者;如果通道为接收而关闭,则引发ClosedReceiveChannel异常。
- tryReceive 如果此通道不为空,则从中检索并删除元素,返回成功结果;如果通道为空,则返回失败结果;如果通道关闭,则返回关闭结果。
- receiveCatching 如果此通道不为空,则从中检索并删除元素,返回成功结果;如果通道为空,则返回失败结果;如果通道关闭,则返回关闭的原因。
- isEmpty 判断通道是否为空
- isClosedForReceive 判断通道是否已经关闭,如果关闭,调用 receive 会引发异常。
- cancel(cause: CancellationException? = null) 以可选原因取消接收此频道的剩余元素。此函数用于关闭通道并从中删除所有缓冲发送的元素。
- iterator() 返回通道的迭代器
2.2 创建不同类型的 Channel
Kotlin 协程库中定义了多个 Channel 类型,所有channel类型的receive方法都是同样的行为: 如果channel不为空, 接收一个元素, 否则挂起。
它们的主要区别在于:
- 内部可以存储元素的数量
- send 是否可以被挂起
Channel 的不同类型:
- Rendezvous channel: 0尺寸buffer (默认类型).
- Unlimited channel: 无限元素, send不被挂起.
- Buffered channel: 指定大小, 满了之后send挂起.
- Conflated channel: 新元素会覆盖旧元素, receiver只会得到最新元素, send永不挂起.
创建 Channel:
val rendezvousChannel = Channel<String>()
val bufferedChannel = Channel<String>(10)
val conflatedChannel = Channel<String>(CONFLATED)
val unlimitedChannel = Channel<String>(UNLIMITED)
三、协程间通过 Channel 实现通信
3.1 多个协程访问同一个 Channel
在协程外部定义 Channel, 就可以多个协程可以访问同一个channel,达到协程间通信的目的。
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
launch {
for (x in 1..5) channel.send(x)
}
launch {
delay(10)
for (y in channel) {
println(" 1 --> $y")
}
}
launch {
delay(20)
for (y in channel) {
println(" 2 --> $y")
}
}
launch {
delay(30)
for (x in 90..100) channel.send(x)
channel.close()
}
}
3.2 produce 和 actor
在协程外部定义 Channel,多个协程同时访问 Channel, 就可以实现生产者消费者模式。
produce
使用 produce 可以更便捷地构造生产者
fun main() = runBlocking<Unit> {
val receiveChannel: ReceiveChannel<Int> = GlobalScope.produce {
var i = 0
while(true){
delay(1000)
send(i)
i++
}
delay(3000)
receiveChannel.cancel()
}
}
我们可以通过 produce 这个方法启动一个生产者协程,并返回一个 ReceiveChannel,其他协程就可以拿着这个 Channel 来接收数据了。
actor
actor 可以用来构建一个消费者协程
fun main() = runBlocking<Unit> {
val sendChannel: SendChannel<Int> = actor<Int> {
for (ele in channel)
ele
}
}
delay(2000)
sendChannel.close()
}
注意:不要在循环中使用 receive ,思考为什么?
produce 和 actor 与 launch 一样都被称作“协程启动器”。通过这两个协程的启动器启动的协程也自然的与返回的 Channel 绑定到了一起,因此 Channel 的关闭也会在协程结束时自动完成,以 produce 为例,它构造出了一个 ProducerCoroutine 的对象
3.3 扇入和扇出
多个协程可能会从同一个channel中接收值,这种情况称为Fan-out。
多个协程可能会向同一个channel发射值,这种情况称为Fan-in。
3.4 BroadcastChannel
3.4.1 BroadcastChannel 基本使用
3.1 中例子提到一对多的情形,从数据处理本身来讲,有多个接收端的时候,同一个元素只会被一个接收端读到。而 BroadcastChannel 则不然,多个接收端不存在互斥现象。
public interface BroadcastChannel<E> : SendChannel<E> {
public fun openSubscription(): ReceiveChannel<E>
public fun cancel(cause: CancellationException? = null)
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility only")
public fun cancel(cause: Throwable? = null): Boolean
}
public fun <E> BroadcastChannel(capacity: Int): BroadcastChannel<E> =
when (capacity) {
0 -> throw IllegalArgumentException("Unsupported 0 capacity for BroadcastChannel")
UNLIMITED -> throw IllegalArgumentException("Unsupported UNLIMITED capacity for BroadcastChannel")
CONFLATED -> ConflatedBroadcastChannel()
BUFFERED -> ArrayBroadcastChannel(CHANNEL_DEFAULT_CAPACITY)
else -> ArrayBroadcastChannel(capacity)
}
创建 BroadcastChannel
创建 BroadcastChannel 需要指定缓冲区大小
val broadcastChannel = broadcastChannel<Int>(5)
订阅 broadcastChannel
订阅 broadcastChannel,那么只需要调用
val receiveChannel = broadcastChannel.openSubscription()
这样我们就得到了一个 ReceiveChannel,获取订阅的消息,只需要调用它的 receive。
3.4.2 使用拓展函数转换
使用 Channel 的拓展函数,也可以将一个 Channel 转换成 BroadcastChannel, 需要指定缓冲区大小。
val channel = Channel<Int>()
val broadcast = channel.broadcast(3)
这样发射给原 channel 的数据会被读取后发射给转换后的 broadcastChannel。如果还有其他协程也在读这个原始的 Channel,那么会与 BroadcastChannel 产生互斥关系。
3.4.3 过时的 API
BroadcastChannel 源码中的说明:
Note: This API is obsolete since 1.5.0. It will be deprecated with warning in 1.6.0 and with error in 1.7.0. It is replaced with SharedFlow.
BroadcastChannel 对于广播式的任务来说有点太复杂了。使用通道进行状态管理时会出现一些逻辑上的不一致。例如,可以关闭或取消通道。但由于无法取消状态,因此在状态管理中无法正常使用!
所以官方决定启用 BroadcastChannel。BroadcastChannel 被标记为过时了,在 kotlin 1.6.0 版本中使用将显示警告,在 1.7.0 版本中将显示错误。请使用 SharedFlow 和 StateFlow 替代它。
关于 SharedFlow 和 StateFlow 将在下文中讲到。
Kotlin 协程二 —— 通道 Channel的更多相关文章
- Kotlin协程通信机制: Channel
Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...
- Kotlin 协程一 —— 全面了解 Kotlin 协程
一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...
- goroutine 分析 协程的调度和执行顺序 并发写 run in the same address space 内存地址 闭包 存在两种并发 确定性 非确定性的 Go 的协程和通道理所当然的支持确定性的并发方式(
package main import ( "fmt" "runtime" "sync" ) const N = 26 func main( ...
- [Golang]-5 协程、通道及其缓冲、同步、方向和选择器
目录 协程 通道 通道缓冲 通道同步 通道方向 通道选择器 协程 Go 协程 在执行上来说是轻量级的线程. 代码演示 import ( "fmt" "time" ...
- 『GoLang』协程与通道
作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算)和程序的并发.程序可以在不同的处理器和计算机上同时执行不同的代码段.Go 语言为构建并发程序的基本代码块是 ...
- rxjava回调地狱-kotlin协程来帮忙
本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- Python自动化开发 -进程、线程和协程(二)
本节内容 一.线程进程介绍 二. 线程 1.线程基本使用 (Threading) 2.线程锁(Lock.RLock) 3.信号量(Semaphore) 4.事件(event) 5.条件(Conditi ...
- Kotlin协程第一个示例剖析及Kotlin线程使用技巧
Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...
- Retrofit使用Kotlin协程发送请求
Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...
随机推荐
- [转帖]Linux系统下cpio命令详解
简介 cpio主要是解压或者将文件压缩到指定文件中即copy-in和copy-out模式. 参数说明 参数 参数说明 -i copy-in模式,解压文件 -o copy-out模式,即压缩文件 -d ...
- 【转帖】淫技巧 | 如何查看已连接的wifi密码
主题使用方法:https://github.com/xitu/juejin-markdown-themes theme: juejin highlight: github 一.引言 在实际工作中,常常 ...
- [转帖]linux性能检测之sar详解
http://blog.51niux.com/?id=99 sar也是sysstat中的一员. 一.介绍 1.1 简介 sar是一个优秀的一般性能监视工具,它可以输出Linux所完成的几乎所有工作的数 ...
- 浅析大促备战过程中出现的fullGc,我们能做什么?
作者:京东科技 白洋 前言: 背景: 为应对618.双11大促,消费金融侧会根据零售侧大促节奏进行整体系统备战.对核心流量入口承载的系统进行加固优化,排除系统风险,保证大促期间系统稳定. 由于大促期间 ...
- elementui-自定表头和在input中遇见的问题
第一个问题:无法关闭 弹出框 <el-table :data="tableData" style="width: 100%"> <el-tab ...
- Typora 1.6.7永久激活
介绍Typora介绍 具体看上面的我就不多介绍了 接下来我们开始教程 需要的文件 Typora安装包 破解补丁包 安装包下载 破解补丁下载 接下来我们全部下载后获得一个安装包一个补丁 安装包直接安装就 ...
- 5.2 Windows驱动开发:内核取KERNEL模块基址
模块是程序加载时被动态装载的,模块在装载后其存在于内存中同样存在一个内存基址,当我们需要操作这个模块时,通常第一步就是要得到该模块的内存基址,模块分为用户模块和内核模块,这里的用户模块指的是应用层进程 ...
- 5.14 汇编语言:仿写Switch选择结构
选择结构,也称为switch语句,是计算机编程中的一种控制结构,用于根据表达式的值选择不同的执行路径.它允许程序根据表达式的值来决定执行哪个代码块,从而实现多分支选择逻辑.switch语句由一个表达式 ...
- C#中DataTable数据导出为HTML格式文件
/// <summary> /// DataTable导出为HTML的Table并保存到本地 /// </summary> /// <param name="d ...
- 1.变量和简单的数据类型--《Python编程:从入门到实践》
1.1 变量 在Python中使用变量时,需要遵守一些规则和指南. 变量名只能包含字母.数字和下划线.变量名可以字母或下划线打头,但不能以数字打 头. 变量名不能包含空格,但可使用下划线来分隔其中的单 ...