基础概念
Deferred 可以把单个数据从一个协程传递给另一个协程,而 Channel 可以传递数据流。
Channel 在概念上与 BlockingQueue 非常相似,但是不同于 BlockingQueue 的 put() 和 take() 函数是阻塞的,Channel 的 send() 和 receive() 函数是挂起函数。
看下面的代码:
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
// this might be heavy CPU-consuming computation or async logic,
// we'll just send five squares
for (x in 1..5) channel.send(x * x)
}
// here we print five received integers:
repeat(5) { println(channel.receive()) }
println("Done!")
}
打印如下:
1
4
9
16
25
Done!
可以看到,使用 Channel 很方便地实现了两个协程的多个数据的传输。
关闭和遍历 Channel
不同于队列,Channel 可以关闭,接收方可以使用 for 循环来接收数据。
关闭就好像给 Channel 发送了一个特殊的 close token,一旦接收方收到 close token,遍历就会停止,这样可以保证所有在调用 close() 函数之前的数据可以接收到。
fun main() = runBlocking { // this: CoroutineScope
val channel = Channel<Int>()
launch {
for (x in 1..3) channel.send(x)
channel.close() // we're done sending
for (x in 4..5) channel.send(x)
}
// here we print received values using `for` loop (until the channel is closed)
for (y in channel) println(y)
println("Done!")
}
打印如下:
1
2
Exception in thread "main" kotlinx.coroutines.channels.ClosedSendChannelException: Channel was closed
at kotlinx.coroutines.channels.Closed.getSendException(AbstractChannel.kt:1107)
...
由于 send() 函数是一个挂起函数,不会阻塞当前协程,所以这里没有打印 3。
建造 Channel 的生产者
协程产生一连串数据的情况很常见,常见于生产者消费者模式,你可以把这样的生产者抽象成一个以 Channel 作为参数的函数,但这违背了函数应该返回结果的常规思维。
你可以通过名为 produce() 的建造协程的函数实现生产者,然后通过 consumeEach() 函数来实现消费者取代 for 循环。代码如下:
fun CoroutineScope.produceSquares(): ReceiveChannel<Int> = produce {
for (x in 1..5) send(x * x)
}
fun main() = runBlocking {
val squares = produceSquares()
squares.consumeEach { println(it) }
println("Done!")
}
Pipelines
Pipelines(管道)是一种协程生产数据流的模式,这个数据流可以是无穷无尽的。
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 1
while (true) send(x++) // infinite stream of integers starting from 1
}
另一个协程消费数据流,进行一些处理,然后生成新的数据,如下例所示,会将收到的数据乘方:
fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
for (x in numbers) send(x * x)
}
使用下面的代码将管道连接起来:
fun main() = runBlocking {
val numbers = produceNumbers() // produces integers from 1 and on
val squares = square(numbers) // squares integers
repeat(5) {
println(squares.receive()) // print first five
}
println("Done!") // we are done
coroutineContext.cancelChildren() // cancel children coroutines
}
这样初始数据就进行了两次处理后打印出来。
使用管道生成质数
下面是一个使用管道生成质数的例子。
以下代码用于生成从 start 开始的整数:
fun CoroutineScope.numbersFrom(start: Int) = produce<Int> {
var x = start
while (true) send(x++) // infinite stream of integers from start
}
然后过滤接收到的数字,移除所有可以被前面的质数整除的数字:
fun CoroutineScope.filter(numbers: ReceiveChannel<Int>, prime: Int) = produce<Int> {
// 遍历前面的质数,如果新的数字不可以整除前面的质数,发送出去
for (x in numbers) if (x % prime != 0) send(x)
}
使用下面的代码将管道连起来:
fun main() = runBlocking {
var cur = numbersFrom(2)
repeat(10) {
val prime = cur.receive()
println(prime)
cur = filter(cur, prime)
}
coroutineContext.cancelChildren() // cancel all children to let main finish
}
这段代码会打印前 10 个质数,整个管道在主线程的上下文中执行,由于所有的协程都在 runBlocking 协程的作用域内启动,我们可以直接调用 cancelChildren() 函数一次性取消所有启动的子协程。
Fan-out(一对多)
多个协程可以从同一个管道接收数据,下面是一个示例,一个生产者协程周期性地产生整数:
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 1 // start from 1
while (true) {
send(x++) // produce next
delay(100) // wait 0.1s
}
}
然后我们可以有几个消费的协程,如下例所示,直接打印 Channel 的 id 和接收到的数据:
fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch {
for (msg in channel) {
println("Processor #$id received $msg")
}
}
然后我们启动 5 个处理的协程,运行时长大概 1 秒,看看会发生什么情况:
fun main() = runBlocking<Unit> {
val producer = produceNumbers()
repeat(5) { launchProcessor(it, producer) }
delay(950)
producer.cancel() // cancel producer coroutine and thus kill them all
}
打印如下:
Processor #0 received 1
Processor #0 received 2
Processor #1 received 3
Processor #2 received 4
Processor #3 received 5
Processor #4 received 6
Processor #0 received 7
Processor #1 received 8
Processor #2 received 9
这里 cancel 一个生产者协程可以关闭它的 Channel,因此可以终止处理的协程。
此外,要注意我们如何在 launchProcessor 代码中使用 for 循环显式迭代 Channel 以执行 fan-out。与 consumeEach 不同,这种 for 循环模式在多个协程中使用是安全的。如果其中一个 Processor 协程失败,其他协程仍将继续处理 Channel,而通过 consumeEach 编写的处理器在正常或异常完成时总是 cancel 对应的 Channel。
Fan-in(多对一)
多个协程可以发送数据给同一个 Channel,如下所示:
fun main() = runBlocking {
val channel = Channel<String>()
launch { sendString(channel, "foo", 200L) }
launch { sendString(channel, "BAR!", 500L) }
repeat(6) { // receive first six
println(channel.receive())
}
coroutineContext.cancelChildren() // cancel all children to let main finish
}
suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) {
while (true) {
delay(time)
channel.send(s)
}
}
缓冲 Channel
前面的 Channel 都没有缓冲,无缓冲的 Channel 在发送方和接收方建立连接的时候传输数据,如果先调用 send,会挂起直到调用 receive;如果先调用 receive,它会被挂起直到 send 调用。
Channel 的工厂方法和 produce 构建器都可以传入一个 capacity 参数来表示缓冲区大小。这样可以让发送方在挂起之前发送多个数据,类似于具有指定容量的 BlockingQueue,BlockingQueue 的容量满了会阻塞。
看下面的代码:
fun main() = runBlocking<Unit> {
val channel = Channel<Int>(4) // create buffered channel
val sender = launch { // launch sender coroutine
repeat(10) {
println("Sending $it") // print before sending each element
channel.send(it) // will suspend when buffer is full
}
}
// don't receive anything... just wait....
delay(1000)
sender.cancel() // cancel sender coroutine
}
打印如下:
Sending 0
Sending 1
Sending 2
Sending 3
Sending 4
最初的 4 个数据会加入到缓冲区,然后在发送第 5 个数据的时候发送方会挂起。
Channel 是公平的
从多个协程调用 Channel 的顺序来看,Channel 的发送和接收操作都是公平的。数据先进先出,第一个调用 receive 的协程第一个拿到数据。下面的示例中,两个协程 ping 和 pong 接收 table channel 发送的 ball 数据:
data class Ball(var hits: Int)
fun main() = runBlocking {
val table = Channel<Ball>() // a shared table
launch { player("ping", table) }
launch { player("pong", table) }
table.send(Ball(0)) // serve the ball
delay(1000) // delay 1 second
coroutineContext.cancelChildren() // game over, cancel them
}
suspend fun player(name: String, table: Channel<Ball>) {
for (ball in table) { // receive the ball in a loop
ball.hits++
println("$name $ball")
delay(300) // wait a bit
table.send(ball) // send the ball back
}
}
打印如下:
ping Ball(hits=1)
pong Ball(hits=2)
ping Ball(hits=3)
pong Ball(hits=4)
协程 ping 先启动,所以它是第一个接收到 ball 的协程。协程 pong 第二个开始接收数据,因为它早就已经在等待了。
Ticker Channel
Ticker Channel 是一种特殊的 Channel,使用该 Channel 后,每次经过给定的延迟时都会产生 Unit。虽然看起来可能毫无用处,但它是一个有用的构建块,可以创建复杂的基于时间的生产管道和操作符,进行窗口和其他依赖时间的处理。
要想创建这样的 Channel,需要使用工厂方法 ticker(),如果不再需要新的数据,使用 ReceiveChannel 的 cancel() 方法。
fun main() = runBlocking<Unit> {
val tickerChannel = ticker(delayMillis = 200, initialDelayMillis = 0) // create a ticker channel
var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
println("Initial element is available immediately: $nextElement") // no initial delay
nextElement = withTimeoutOrNull(100) { tickerChannel.receive() } // all subsequent elements have 200ms delay
println("Next element is not ready in 100 ms: $nextElement")
nextElement = withTimeoutOrNull(120) { tickerChannel.receive() }
println("Next element is ready in 200 ms: $nextElement")
// Emulate large consumption delays
println("Consumer pauses for 300ms")
delay(300)
// Next element is available immediately
nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
println("Next element is available immediately after large consumer delay: $nextElement")
// Note that the pause between `receive` calls is taken into account and next element arrives faster
nextElement = withTimeoutOrNull(120) { tickerChannel.receive() }
println("Next element is ready in 100ms after consumer pause in 300ms: $nextElement")
tickerChannel.cancel() // indicate that no more elements are needed
}
打印如下:
Initial element is available immediately: kotlin.Unit
Next element is not ready in 100 ms: null
Next element is ready in 200 ms: kotlin.Unit
Consumer pauses for 300ms
Next element is available immediately after large consumer delay: kotlin.Unit
Next element is ready in 100ms after consumer pause in 300ms: kotlin.Unit
通过代码可以发现 ticker 可以意识到消费者处理数据的暂停操作,当暂停发生时,调整下一个产生数据的延迟,这样可以让产生数据保持在一个固定的频率内。