kotlin coroutine文档:通道

通道

延迟值提供了在协同程序之间传输单个值的便捷方法。 通道提供了一种传输值流的方法。

通道是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,以维持元素之间的固定延迟。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值