协程 channel 官方文档 翻译

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都会被其消耗,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值