一、前言
在使用线程的时候有时候我们需要在两个线程之间进行通信,协程也是如此。在协程上面通信的话需要使用Channel。Channel既可以一个协程对一个协程,也可以多个协程对多个协程。通常发送信息的协程被称为生产者接收信息的协程被称为消费者,需要注意的是每条消息只会被处理一次,然后就从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
是通过SendChannel
和ReceiveChannel
来实现发送和接收功能的。他们的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 可以保持固定元素之间的延迟。