Kotlin协程Channel(四)

一、前言

在使用线程的时候有时候我们需要在两个线程之间进行通信,协程也是如此。在协程上面通信的话需要使用ChannelChannel既可以一个协程对一个协程,也可以多个协程对多个协程。通常发送信息的协程被称为生产者接收信息的协程被称为消费者,需要注意的是每条消息只会被处理一次,然后就从Channel删除掉了

二、Channel

Channel的实现如下

interface SendChannel<in E> {
    suspend fun send(element: E)
    fun close(): Boolean
}

interface ReceiveChannel<out E> {
    suspend fun receive(): E
}    

interface Channel<E> : SendChannel<E>, ReceiveChannel<E>

Channel是通过SendChannelReceiveChannel来实现发送和接收功能的。他们的send(element: E)receive()是被suspend修饰的,所以这两个函数是可以被挂起的。系统默认定义了四种类型的Channel

  • Unlimited channel

    无限Channel表示七通道中可以容纳的元素是无限的,直至内存不足

  • Buffered channel

    缓冲Channel表示大小是受限制的,当通道满时,下次的send调用会暂停,直到出现更多可用空间

  • “Rendezvous” channel

    “Rendezvous” channel这是一个没有缓冲区的通道,也就是说缓冲大小为0,发送和接收的函数总是被挂起,直到另外一个被调用。

  • Conflated channel

    合并通道发送到合并通道的新元素将覆盖先前发送的元素,因此接收器将始终只获取最新的元素。的send通话将永远不会停止。

他们的创建方式如下

val rendezvousChannel = Channel<String>()
val bufferedChannel = Channel<String>(10)
val conflatedChannel = Channel<String>(CONFLATED)
val unlimitedChannel = Channel<String>(UNLIMITED)

通道都是有容量的,所以这四种通道都会有这三种状态空、有数据、满。上文说到他们的send(element: E)receive()是被suspend修饰的,所以这两个函数是可以被挂起的。故名思义就是,假设我们创建了一个通道,如果发送数据时候,通道已经满了,那么程序就会阻塞直到出现可用空间。如果接收消息时候,生产者并没有生产那么多消息,那么就会程序阻塞直到出现足够多的消息。其余场景并不会阻塞程序,哪怕生产的消息并没有被消费完。除非它是合并通道val conflatedChannel = Channel<String>(CONFLATED)。下面我们看演示例子

  • 生产者大于通道容量的例子

    @Test
    fun channelTest(){
        val channel = Channel<String>(2)
        runBlocking {
            launch {
                channel.send("a")
                channel.send("b")
              	channel.send("b")
              	channel.send("d")
              //生产了四条消息,然后消费者处理了一个,还有三个,通道容量为2,多出一个,所以这个程序就会一直阻塞无法结束
            }
            launch {
                repeat(1){
                    val receiver = channel.receive()
                    println("---end:$receiver")
                }
            }
        }
       channel.close()
       println("---end")
     }
    
  • 消费者大于生产者的例子

    @Test
     fun channelTest(){
        val channel = Channel<String>(2)
        runBlocking {
            launch {
                channel.send("a")
            }
            launch {
    
                repeat(2){
                    val receiver = channel.receive()
                    println("---end:$receiver")
                }
            }
        }
        channel.close()
        println("---end")
    }
    

以上两种情况都会阻塞程序直到两端保持平衡,其余情况则不会

三、关闭与迭代通道

Channel是可以使用close关闭的。需要注意的是关闭后就不可以再发送数据了,否则会出现异常。接收没有问题,但是没有接收的数据可能也接收不到,所以需要再接收完后再关闭。之前看到Channel接收数据时需要知道发送了几次,其实Channel也是支持遍历的。

val channel = Channel<Int>()
launch {
    for (x in 1..5) channel.send(x * x)
    channel.close() // 我们结束发送
}
// 这里我们使用 `for` 循环来打印所有被接收到的元素(直到通道被关闭)
for (y in channel) println(y)
println("Done!")

四、produce

之前看到每次都要实例化一个channel对象。然后用for循环接收。官方对此做了更简单的方式,使用produce进行构造channel。使用拓展函数consumeEach()函数来替代for循环

   @Test
    fun flowChannel(){
        runBlocking {
            val squares = produceSquares()
            squares.consumeEach { println(it) }
            println("Done!")
        }
    }
    fun CoroutineScope.produceSquares(): ReceiveChannel<Int> = produce {
        for (x in 1..5) send(x * x)
    }

五、对其它channel的处理

通过对produce的拓展,我们还可以对其它的Channel进行处理

fun main() = runBlocking {
    val numbers = produceNumbers() // 从 1 开始生成整数
    val squares = square(numbers) // 整数求平方
    repeat(5) {
        println(squares.receive()) // 输出前五个
    }
    println("Done!") // 至此已完成
    coroutineContext.cancelChildren() // 取消子协程
}

fun CoroutineScope.produceNumbers() = produce<Int> {
    var x = 1
    while (true) send(x++) // 从 1 开始的无限的整数流
}

fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
    for (x in numbers) send(x * x)
}

六、扇出

多个协程可以对单个channel进行处理,来达到分布式的效果

fun main() = runBlocking<Unit> {
    val producer = produceNumbers()
    repeat(5) { launchProcessor(it, producer) }
    delay(950)
    producer.cancel() // 取消协程生产者从而将它们全部杀死
}

fun CoroutineScope.produceNumbers() = produce<Int> {
    var x = 1 // start from 1
    while (true) {
        send(x++) // 产生下一个数字
        delay(100) // 等待 0.1 秒
    }
}

fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch {
    for (msg in channel) {
        println("Processor #$id received $msg")
    }    
}

运行结果如下

Processor #2 received 1
Processor #4 received 2
Processor #0 received 3
Processor #1 received 4
Processor #3 received 5
Processor #2 received 6
Processor #4 received 7
Processor #0 received 8
Processor #1 received 9
Processor #3 received 10

需要注意的是,如果关闭生产者的协程的通道,那么就会终止正在使用这个通道的迭代。另外可以发现这里并没有使用consumeEach,因为通过consumeEach 编写的处理器始终在正常或非正常完成时消耗(取消)底层通道。而for却不会

七、扇入

有扇出,也有扇入。将多个协程也可以发送到同一个通道称之为扇入。如下

fun main() = runBlocking {
    val channel = Channel<String>()
    launch { sendString(channel, "foo", 200L) }
    launch { sendString(channel, "BAR!", 500L) }
    repeat(6) { // 接收前六个
        println(channel.receive())
    }
    coroutineContext.cancelChildren() // 取消所有子协程来让主协程结束
}

suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) {
    while (true) {
        delay(time)
        channel.send(s)
    }
}

输出结果如下

foo
foo
BAR!
foo
foo
BAR!

八、缓冲

之前的例子会有个问题。之前已经说过通道容量,那个就是缓冲

九、计时器通道

计时器通道是一种特别的会合通道,每次经过特定的延迟都会从该通道进行消费并产生 Unit。主要用来对基于时间的分片操作和其它时间相关的处理。

fun main() = runBlocking<Unit> {
    val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) //创建计时器通道
    var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
    println("Initial element is available immediately: $nextElement") // no initial delay

    nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // all subsequent elements have 100ms delay
    println("Next element is not ready in 50 ms: $nextElement")

    nextElement = withTimeoutOrNull(60) { tickerChannel.receive() }
    println("Next element is ready in 100 ms: $nextElement")

    // 模拟大量消费延迟
    println("Consumer pauses for 150ms")
    delay(150)
    // 下一个元素立即可用
    nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
    println("Next element is available immediately after large consumer delay: $nextElement")
    // 请注意,`receive` 调用之间的暂停被考虑在内,下一个元素的到达速度更快
    nextElement = withTimeoutOrNull(60) { tickerChannel.receive() } 
    println("Next element is ready in 50ms after consumer pause in 150ms: $nextElement")

    tickerChannel.cancel() // 表明不再需要更多的元素
}

演示效果如下

Initial element is available immediately: kotlin.Unit
Next element is not ready in 50 ms: null
Next element is ready in 100 ms: kotlin.Unit
Consumer pauses for 150ms
Next element is available immediately after large consumer delay: kotlin.Unit
Next element is ready in 50ms after consumer pause in 150ms: kotlin.Unit

请注意,ticker 知道可能的消费者暂停,并且默认情况下会调整下一个生成的元素如果发生暂停则延迟,试图保持固定的生成元素率。

给可选的 mode 参数传入 TickerMode.FIXED_DELAY 可以保持固定元素之间的延迟。

十、参考链接

  1. Channel

    https://kotlinlang.org/docs/channels.html

  2. 通道

    https://www.kotlincn.net/docs/reference/coroutines/channels.html

  3. 协程和通道介绍

    https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/08_Channels

  4. Kotlin 协程系列文章导航:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值