Channel提供了一种便捷的方法使单个值在多个协程之间进行相互传输。 通道提供了一种在流中传输值的方法。
简单说就是Channel用于多个协程之间的通信,让协程之间的协作更加的顺畅。
主要内容:
一、通道基本使用
Channel 跟BlockingQueue 非常相似,其中一个不同是它代替了阻塞的 put(…) 操作并提供了挂起的 send(…),还替代了阻塞的 take(…) 操作并提供了挂起的 receive(…)。
简单说就是创建Channel,send发送数据,receive接收数据。
//创建Channel
val channel = Channel<Int>()
launch {
// send发送数据
for (x in 1..5) channel.send(x * x)
}
// receive接收数据
repeat(5) { println(channel.receive()) }
println("Done!")
打印结果
1
4
9
16
25
Done!
二、通道的缓冲(容量)
上面的例子展示的通道都是没有缓冲区的。如果发送先被调用,则它将被挂起直到接收被调用, 如果接收先被调用,它将被挂起直到发送被调用。
Channel() 工厂函数与 produce 建造器通过一个可选的参数 capacity 来指定 缓冲区大小 。缓冲允许发送者在被挂起前发送多个元素。
如果,缓冲区满了,并且也一直没有调用receive接收,send就会挂起。
val channel = Channel<Int>(4) // 启动带缓冲的通道
val sender = launch { // 启动发送者协程
repeat(10) {
println("Sending $it")
channel.send(it)
}
}
// 没有接收到东西……只是等待……
delay(1000)
sender.cancel() // 取消发送者协程
打印结果
Sending 0
Sending 1
Sending 2
Sending 3
Sending 4
通过打印结果,我们看到,前四个元素被加入到了缓冲区并且发送者在试图发送第五个元素的时候被挂起。
capacity的其它固定类型
- RENDEZVOUS,默认类型;send数据后,如果,一直没有receive的话,就会挂起。
- CONFLATED,新元素会替代旧元素,receive一直会获取最新的数据
- UNLIMITED,缓冲区没有限制;一直可以send数据
- BUFFERED,指定元素大小,默认为64,当满了后Send会被挂起(可通过程序参数修改)。
三、迭代Channel
Channel类似与序列,我们在读取的时候可以直接获取Channel的迭代器(iterator)
通过迭代器(iterator)或者for循环,可以接收Channel产生的多个元素。
/**
* 不限制容量
*
* 通过迭代器遍历,获取数据
*/
fun simpleChannel3() {
val channel = Channel<Int>(Channel.UNLIMITED)
launch {
// 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送
for (x in 1..5) channel.send(x * x)
}
// 这里我们打印了 5 次被接收的整数:
launch {
val iterator = channel.iterator()
while (iterator.hasNext()) {
Log.d("liu", "Channel send :${iterator.next()}")
delay(1000)
}
// //这种方式也可以
// for (data in channel) {
// Log.d("liu", "Channel send :${data}")
// delay(1000)
// }
}
Log.d("liu", "Done!")
}
四、produce与actor
前面所有示例,我们都是通过创建Channel。然后,通过不同协程进行操作。那么,有没有构建生产者与消费者的便捷方法呢?
我们可以通过produce方法启动一个生产者协程,并返回一个ReceiceChannel,其它的协程就可以用这个Channel来接受数据了。反过来,我们可以用actor启动一个消费者协程,返回一个SendChannel,其他协程通过这个Channel来发送数据。
不过,笔者使用的版本,这2个API都是带标记的,还没有转正。
- produce
fun simpleChannelProduct() {
//scope.produce。这里直接委托使用了MainScope
//@ExperimentalCoroutinesApi 带标记的
val receiveChannel = produce<Int> {
var i = 0
repeat(10) {
delay(100)
send(i++)
}
}
launch {
for (data in receiveChannel) {
Log.d("liu", "Channel receive :${data}")
}
}
}
使用名为 produce 的便捷的协程构建器,可以很容易的在生产者端正确工作, 并且我们使用扩展函数 consumeEach 在消费者端替代 for 循环:
fun simpleChannelProduct2() {
val receiveChannel = produce<Int> {
var i = 0
repeat(10) {
delay(100)
send(i++)
}
}
launch {
receiveChannel.consumeEach {value ->
Log.d("liu", "Channel receive :${value}")
}
}
}
- actor
fun simpleChannelSend() {
//@ObsoleteCoroutinesApi 带标记的
val sendChannel = actor<Int> {
while (true) {
val data = receive()
Log.d("liu", "Channel receive :${data}")
}
}
launch {
for (i in 0..3) {
delay(100)
sendChannel.send(i++)
}
}
}
五、通道Channel的关闭
produce和actor返回的Channel都会随着对应的协程执行完毕而关闭,因此,Channel也称为热数据流
对于Channel,如果我们调用了它的Close方法,它会立即停止接受新元素,也就是说,这时它的isClosedForSend会立即返回true,而由于Channel缓冲区的存在,这时候可能有些数据还没有被处理,因此要等到所有元素都被读取后isColsedForReceive返回返回true,Channel才会被关闭。
我们瞬间发送3个,发送完毕立即close。接受的时候,每隔1秒接受一个。这样,在关闭的时候,Channel中是有数据的,我们看下Channel的状态
示例代码,
/**
* Channel的关闭
*/
fun simpleChannelClose() {
val channel = Channel<Int>(3)
launch {
List(3) {
channel.send(it)
Log.d("liu", "Channel send :${it}")
}
channel.close()
Log.d("liu", "Channel isClosedForSend :${channel.isClosedForSend}")
Log.d("liu", "Channel isClosedForReceive :${channel.isClosedForReceive}")
Log.d("liu", "Channel isEmpty :${channel.isEmpty}")
}
launch {
for (data in channel) {
Log.d("liu", "Channel receive :${data}")
delay(1000)
}
Log.d("liu", "Channel isClosedForSend :${channel.isClosedForSend}")
Log.d("liu", "Channel isClosedForReceive :${channel.isClosedForReceive}")
Log.d("liu", "Channel isEmpty :${channel.isEmpty}")
}
}
六、BroadcastChannel
前面提到,发送端和接收端在Channel中存在一对多的情况,从数据处理本身来讲,虽然有多个接收端,但是同一个元素只会被一个接收端接收。广播则不然,多个接收端都可以接收,不存在互斥行为。
/**
* BroadcastChannel
*/
fun simpleBroadcastChannel() {
val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
//2种创建方式
// val channel = Channel<Int>()
// val broadcastChannel = channel.broadcast(Channel.BUFFERED)
launch {
List(3) {
delay(1000)
broadcastChannel.send(it)
}
}
List(3) {
launch {
val receiveChannel = broadcastChannel.openSubscription()
for (i in receiveChannel) {
Log.d("liu", "$it : Channel is :${i}")
}
}
}
}
七、多路复用(实验性质)
数据通信系统或计算机网络系统中,传输媒体的宽带或容量往往会大于传输单一信号的需求,为了有效的利用通信线路,希望一个信道同时传输多路信号,这就是所谓的多路复用技术。
中文文档:戳一下
await多路复用
2个API分别从网络和本地缓存获取数据,期望哪个先返回就先用哪个。
suspend fun CoroutineScope.obtainDataFromNetwork() = async(Dispatchers.IO) {
delay(500)
"I'm From Network"
}
fun CoroutineScope.obtainDataFromCache() = async(Dispatchers.IO) {
delay(300)
"I'm From Cache"
}
fun awaitTest() {
launch {
val data = obtainDataFromNetwork()
val cacheData = obtainDataFromCache()
/**
* 拿到最快的返回结果,哪个返回快用哪个
*/
val dataResult = select<Test> {
cacheData.onAwait { Test(it, true) }
data.onAwait { Test(it, false) }
}
dataResult.data?.let {
Log.d("liu", "$it ")
}
}
}
Channel多路复用
fun channelTest() {
val channels = listOf(Channel<Int>(), Channel<Int>())
launch {
delay(100)
channels[0].send(0)
}
launch {
delay(200)
channels[1].send(1)
}
launch {
val result = select<Int> {
channels.forEach { channel ->
channel.onReceive { it }
}
Log.d("liu", "$result ")
}
}
}
SelectClause
有哪些事件是可以被select的?其实,所有能够被select的事件SelectClause N的类型:
- SelectClause0:对应事件没有返回值,例如 join,那么onJoin就是事件SelectClauseN类型。使用时,onJoin是一个无参函数
- SelectClause1:对应事件有返回值,前面的onAwait,onReceive都属于这种
- SelectClause2:对应事件有返回值,此外还需要一个额外的参数,例如 Channel.onSend有2个参数,1,就是Channel的数据类型的值,表示即将发送的值;2,发送成功时的回调参数。
如果我们想要确认挂起函数是否支持select函数,只需要查看其是否存在对应的SelectClauseN类型回调即可。