Channels
当在协程之间传递一个单独的值的时候,延迟传递的值是一种方便的形式。Channels提供了一种以流的方式传递这类值的方式。
Channel 基础
Channel 在概念上来说非常类似阻塞队列。它们之间的主要不同是,存放数据的时候,阻塞队列采用阻塞式的 put方法,而Channel使用挂起函数 send方法,获取数据的时候,阻塞队列使用阻塞式的take方法,而Channel使用挂起函数receive。
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可以被关闭,来表明不会再发送数据。接收端可以很简便的使用类似for循环来遍历channel中发送的元素。
从概念上来说,关闭channel更像是发送一个关闭信号给channel。一旦收到关闭信号,channel的迭代处理也就终止了。这是保证所有关闭信号之前的数据都能被接收到的一种方法:
val channel = Channel<Int>()
launch {
for (x in 1..5) channel.send(x * x)
channel.close() // we're done sending
}
// here we print received values using `for` loop (until the channel is closed)
for (y in channel) println(y)
println("Done!")
完整源码点此获取
构造channel中的生产者
这种模式下,协程产生一系列的数据的情况很常见。这是在并发代码中常见的生产者-消费者模式中的一部分。你可以把channel当做参数来抽象化一个生产者,和常识不太一样的地方是,这些函数必须有返回结果。
在生产者这边,协程有一个构造方法叫做produce 来轻松的完成生产者的角色。并且有一个扩展函数叫做consumeEach,用来在消费者这边替代for循环。
fun CoroutineScope.produceSquares(): ReceiveChannel<Int> = produce {
for (x in 1..5) send(x * x)
}
val squares = produceSquares()
squares.consumeEach { println(it) }
println("Done!")
1
4
9
16
25
Done!
完整源码点此获取
管道
管道可以理解为一种模式,在这种模式下,一个协程可能生产无止尽的数据或者数据流:
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)
}
主要代码启动并连接整个管道:
val numbers = produceNumbers() // produces integers from 1 and on
val squares = square(numbers) // squares integers
for (i in 1..5) println(squares.receive()) // print first five
println("Done!") // we are done
coroutineContext.cancelChildren() // cancel children coroutines
完整源码点此获取
所有协程创建的函数,都是 CoroutineScope的扩展函数,因此我们可以信赖这种结构化的并发设计。确保我们的应用不会因为是全局化的协程而卡死。
用管道求素数
我们通过一个例子来把管道使用到极致,这个例子是使用协程中的管道来产生一堆素数。我们从一堆无止尽的数开始。
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)
}
现在我们的管道从2开始发送数据流,从当前的channel获取一个素数,并用一个新的管道接受每个发送过来的素数。
numbersFrom(2) -> filter(2) -> filter(3) -> filter(5) -> filter(7) ...
下面的代码打印了前10个素数,在主线程执行整个管道。所有启动的协程都在runBlocking 协程的协程范围内,我们没必要保存一个已启动的协程的列表。只需使用cancelChildren 这个扩展函数,在打印前十个素数之后,就可以取消所有子协程
var cur = numbersFrom(2)
for (i in 1..10) {
val prime = cur.receive()
println(prime)
cur = filter(cur, prime)
}
coroutineContext.cancelChildren() // cancel all children to let main finish
完整源码点此获取
输出结果是:
2
3
5
7
11
13
17
19
23
29
注意,你同样可以使用标准库中的iterator 协程来构造类似的管道。用iterator 函数来代替 produce 函数,用yield函数来代替send函数, 用next函数来代替receive函数,用Iterator来代替ReceiveChannel, 来处理协程的执行时的协程范围问题。没必要一定使用runBlocking 的协程范围。上述使用管道的好处在于,如果你使用的是Dispatchers.Default 他可以利用多个cpu核心来完成任务,提高效率。
不管咋样,查找素数都不像一个实战例子。实战中,管道会涉及到一些挂起的调用(比如从远程服务异步获取数据),并且这些管道不太可能是通过 sequence/iterator 来构建的。这些管道可能不允许随意挂起,这些管道不像produce方法那样, produce方法是完全异步的调用
Fan-out
(译者注,这个词更像是指一种安全策略,多个协程去竞争数据并处理结果,但不会由于他们的竞争导致处理结果的乱序。处理结果的顺序依然是同发送顺序一致)
多个协程也许会收到同一channel传来的数据,并且他们可能需要协作来完成工作。我们开启一个生产者协程,周期性的发送整数(每秒发送10个数字)
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 1 // start from 1
while (true) {
send(x++) // produce next
delay(100) // wait 0.1s
}
}
然后我们可以有几个处理协程来处理这些数据。在本例中,处理协程只是打印他们自己的id和接收到的数字。
fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch {
for (msg in channel) {
println("Processor #$id received $msg")
}
}
现在我们启动五个处理协程,并让他们几乎同时工作,看看会怎样:
val producer = produceNumbers()
repeat(5) { launchProcessor(it, producer) }
delay(950)
producer.cancel() // cancel producer coroutine and thus kill them all
完整源码点此获取
输出会类似如下,尽管收到具体某个数处理器的id可能会不一样。
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
注意记得取消生产者协程和关闭他的channel, 以便最后终止处理协程的工作。
留意我们是如何显式调用for循环来在launchProcessor代码中执行 fan-out 操作的。不同于consumeEach, 多个协程使用for循环也是十分安全的。如果任一处理协程发生问题,其他处理协程仍然能够继续处理channel,而如果一个处理协程通过consumeEach来处理的话,不论处理结果成功还是失败,紧接着的channel都会被其消耗,