通道
延迟值提供了在协同程序之间传输单个值的便捷方法。 通道提供了一种传输值流的方法。
通道是kotlinx.coroutines的实验性的功能。 他们的API预计将在接下里的kotlinx.coroutines库更新中变化,并且可能会发生重大变化。
通道基础
Channel在概念上与BlockingQueue非常相似。 一个关键的区别是,它不是阻塞式的put操作,而是具有挂起的send,而不是阻塞式的take操作,它具有挂起的receive。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
//例子开始
val channel = Channel<Int>()
launch {
// 这可能是耗费大量CPU的计算或异步逻辑,我们只发送五个平方
for (x in 1..5) channel.send(x * x)
}
// 这里我们打印了五个接收到的整数
repeat(5) { println(channel.receive()) }
println("Done!")
//例子结束
}
在这里获取完整代码
这段代码的结果如下:
1
4
9
16
25
Done!
关闭和迭代通道
与队列不同,可以关闭通道以表示不再有元素过来。 在接收器端,可以方便地使用常规的for循环接收来自通道的元素。
从概念上讲,close就像向通道发送特殊的关闭令牌。 一旦收到此关闭令牌,迭代就会停止,因此可以保证,关闭令牌前所有先前发送的元素已经收到:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
//例子开始
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,它可以很容易地在生产者端完成,并且扩展函数consumeEach,它取代了消费者端的for循环:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
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!")
//例子结束
}
在这里获取完整代码
管道
管道是一个协程正在生成(可能是无限的)值流的模式:
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)
}
主代码启动并连接整个管道:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
//例子开始
val numbers = produceNumbers() // 生成从1开始的整数
val squares = square(numbers) // 平方数
for (i in 1..5) println(squares.receive()) // 打印前五个
println("Done!") // we are 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)
}
在这里获取完整代码
所有创建协程的函数都被定义为CoroutineScope的扩展,因此我们可以依赖结构化并发来确保,我们的应用程序中没有延迟全局协程。
使用管道的素数
让我们将管道带到极端,通过一个例子,这个例子使用协程间的管道生成素数。 我们从无限的数字序列开始。
fun CoroutineScope.numbersFrom(start: Int) = produce<Int> {
var x = start
while (true) send(x++) // 无限的整数流,从start开始
}
以下管道阶段过滤传入的数字流,删除可由给定素数整除的所有数字:
fun CoroutineScope.filter(numbers: ReceiveChannel<Int>, prime: Int) = produce<Int> {
for (x in numbers) if (x % prime != 0) send(x)
}
现在我们构建我们的管道,通过从2开始的一个数字流,从当前通道获取一个素数,并为找到的每个素数启动新的管道阶段:
numbersFrom(2) -> filter(2) -> filter(3) -> filter(5) -> filter(7) ...
以下示例打印前十个素数,在主线程的上下文中运行整个管道。 由于所有协程都是在主runBlocking协程的范围内启动的,因此我们不必保留我们已经启动的所有协同程序的列表。 在打印前十个素数后,我们使用cancelChildren扩展函数取消所有子协程。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
//例子开始
var cur = numbersFrom(2)
for (i in 1..10) {
val prime = cur.receive()
println(prime)
cur = filter(cur, prime)
}
coroutineContext.cancelChildren() // 为了主协程结束,取消所有子协程
//例子结束
}
fun CoroutineScope.numbersFrom(start: Int) = produce<Int> {
var x = start
while (true) send(x++) // 无限的整数流,从start开始
}
fun CoroutineScope.filter(numbers: ReceiveChannel<Int>, prime: Int) = produce<Int> {
for (x in numbers) if (x % prime != 0) send(x)
}
在这里获取完整代码
这段代码的结果是:
2
3
5
7
11
13
17
19
23
29
请注意,您可以使用标准库中的buildIterator协程生成器构建相同的管道。 用buildIterator替换produce,send替换yield,next替换接收,用Iterator替换receive,Iterator替换ReceiveChannel,并去掉协程作用域。 您也不需要runBlocking。 但是,如上所示使用通道的管道的好处是,如果在Dispatchers.Default上下文中运行它,它实际上可以使用多个CPU内核。
无论如何,这是找到素数的极不切实际的方法。 实际上,管道确实涉及一些其他的挂起调用(比如对远程服务的异步调用),并且这些管道不能使用buildSequence / buildIterator构建,因为它们不允许任意挂起,这与完全异步的produce不同。
扇出
多个协程可以从同一个通道接收,在它们之间分配工作。 让我们从生产者协程开始,它定期生成整数(每秒十个数字):
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 1 // 从1开始
while (true) {
send(x++) // 生产下一个
delay(100) // 等待0.1秒
}
}
然后我们可以有几个处理器协程。 在这个例子中,他们只打印他们的id和收到的数字:
fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch {
for (msg in channel) {
println("Processor #$id received $msg")
}
}
现在让我们启动五个处理器,让它们工作几乎一秒钟。 看看会发生什么:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking<Unit> {
//例子开始
val producer = produceNumbers()
repeat(5) { launchProcessor(it, producer) }
delay(950)
producer.cancel() // 取消生产者协程,因此把他们都杀死了
//例子结束
}
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 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")
}
}
在这里获取完整代码
输出将类似于以下输出,尽管接收每个特定整数的处理器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
注意,取消生成器协程会关闭其通道,从而最终终止对通道的迭代,这个通道是处理器协程正在执行。
另外,请注意我们如何使用for循环显式迭代通道,以在launchProcessor代码中执行扇出。 与consumeEach不同,这种for循环模式可以非常安全地在多个协程中使用。 如果其中一个处理器协程失败,则其他处理器协程仍将处理该通道,而通过consumeEach写入的处理器,在其正常或异常完成时始终消耗(取消)底层通道。
扇出
多个协程可以发送到同一个通道。 例如,让我们有一个字符串通道和一个挂起函数,它以指定的延时重复发送指定字符串到此通道:
suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) {
while (true) {
delay(time)
channel.send(s)
}
}
现在,让我们看看如果我们启动几个协程发送字符串,会发生什么(在这个例子中,我们在主线程的上下文中作为主协程的子协程中启动它们):
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
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!
缓冲式通道
到目前为止展示的通道没有缓冲。 当发送方和接收方彼此相遇(也称为集合点)时,无缓冲的通道传输元素。 如果首先调用send,那么它将被挂起,直到调用receive,如果先调用receive,它将被挂起,直到调用send。
Channel()工厂函数和produce构建器都使用可选的容量参数来指定缓冲区大小。 缓冲区允许发送方在挂起之前发送多个元素,类似于具有指定容量的BlockingQueue,当缓冲区已满时阻塞。
看一下以下代码的行为:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking<Unit> {
//例子开始
val channel = Channel<Int>(4) //创建缓冲式通道
val sender = launch { // 开启sender协程
repeat(10) {
println("Sending $it") // 在发送每个元素之前打印
channel.send(it) // 当缓冲满了的时候挂起
}
}
// 不会接收任何东西... 仅等待...
delay(1000)
sender.cancel() //取消sender协程
//例子结束
}
在这里获取完整代码
它使用容量为4的缓冲通道打印“发送”五次:
Sending 0
Sending 1
Sending 2
Sending 3
Sending 4
前四个元素被添加到缓冲,sender在尝试发送第五个元素时挂起
通道是公平的
对于从多个协同程序调用它们的顺序,向通道发送和接收操作是公平的。 它们作为先进先出的顺序,例如 调用receive的第一个协程获取这个元素。 在以下示例中,两个协程“ping”和“pong”从共享的“表”通道接收“ball”对象。
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
//例子开始
data class Ball(var hits: Int)
fun main() = runBlocking {
val table = Channel<Ball>() // 共享表
launch { player("ping", table) }
launch { player("pong", table) }
table.send(Ball(0)) // 提供球
delay(1000) // 延时一秒
coroutineContext.cancelChildren() // 游戏结束,取消它们
}
suspend fun player(name: String, table: Channel<Ball>) {
for (ball in table) { //在一个循环中接收球
ball.hits++
println("$name $ball")
delay(300) // 等待一会儿
table.send(ball) // 发送回来这个球
}
}
//例子结束
在这里获取完整代码
“ping”协程首先启动,因此它是第一个接收球的人。 即使“ping”协程在将球送回表后立即再次接收球,球也会被“pong”协程接收,因为它已经在等待它:
ping Ball(hits=1)
pong Ball(hits=2)
ping Ball(hits=3)
pong Ball(hits=4)
请注意,由于正在使用的执行子的性质,有时通道可能会产生看起来不公平的执行。 有关详细信息,请参阅此问
Ticker通道
Ticker通道是一个特殊的集合通道,从这个通道消费之后,每次经过给定延迟时它会产生Unit。 虽然它可能看起来没有用,但它是一个有用的构建块,可以创建复杂的基于时间的produce管道和运算符,这些管道和运算符可以进行窗口化和其他与时间相关的处理。 可以在select中使用Ticker通道执行“on tick”操作。
要创建此类渠道,请使用ticker工厂方法。 为了表示不需要其他元素,请使用ReceiveChannel.cancel方法。
现在让我们看看它在实践中是如何运作的:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking<Unit> {
val tickerChannel = ticker(delayMillis = 100, initialDelayMillis = 0) // 创建ticker通道
var nextElement = withTimeoutOrNull(1) { tickerChannel.receive() }
println("Initial element is available immediately: $nextElement") // 初始化延时还没传递
nextElement = withTimeoutOrNull(50) { tickerChannel.receive() } // 所有后续元素有100毫秒延时
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,以维持元素之间的固定延迟。