文章目录
前言:
前几篇文章我们介绍了协程的基础知识、上下文、协程的取消和异常:
第一节:kotlin之协程基础知识
第二节:Kotlin之协程的上下文
第三节:Kotlin之源码解析协程的取消和异常流程
第四节:Kotlin之协程取消和异常的运用
这篇文章我们来介绍一下Channel的使用。
1.Channel的基础知识
通道是一种一个协程在流中开始生产可能无穷多个元素的模式。一个 Channel 是一个和 BlockingQueue 非常相似的概念。其中一个不同是它代替了阻塞的 put 操作并提供了挂起的 send,还替代了阻塞的 take 操作并提供了挂起的 receive。
fun main() {
val scope = CoroutineScope(Dispatchers.IO)
runBlocking {
val channel = Channel<Int>()
scope.launch {
for (i in 1..3) channel.send(i)
channel.close()
}
repeat(3) { println(channel.receive()) }
}
}
// 输出
1
2
3
Channel()是一个顶层函数用来创建不同的Channel对象,send()、receive()都是挂起函数。一个 close 操作就像向通道发送了一个特殊的关闭指令。 这个迭代停止就说明关闭指令已经被接收了。所以这里保证所有先前发送出去的元素都在通道关闭前被接收到。从输出结果来看,channel保持和队列一样的规则,都是先进先出。
2.支持迭代的Channel
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> { }
public interface ReceiveChannel<out E> {
...
public operator fun iterator(): ChannelIterator<E>
...
}
public interface ChannelIterator<out E> {
public suspend operator fun hasNext(): Boolean
public suspend fun next0(): E {
if (!hasNext()) throw ClosedReceiveChannelException(DEFAULT_CLOSE_MESSAGE)
return next()
}
public operator fun next(): E
}
从继承关系我们很容易看出Channel实现了迭代器的功能,因此我们也可以使用for in,来接收元素:
fun main() {
val scope = CoroutineScope(Dispatchers.IO)
runBlocking {
val channel = Channel<Int>()
scope.launch {
for (i in 1..3) channel.send(i)
channel.close()
}
for (element in channel) {
println("element = $element")
}
}
}
// 输出
element = 1
element = 2
element = 3
3.带缓冲的Channel
我们在构造Channel的时候调用了一个名为Channel()的函数,虽然两个“Channel”看起来是一样的,但它确实不是Channel的构造函数。在Kotlin中我们经常定义一个顶层函数来伪装成同名类型的构造器,这本质上就是工厂函数。Channel()函数有一个名为capacity[kəˈpæsɪtɪ]的参数,翻译成中文的意思就是:容量、容积。这在存储数据的一些容器中很常见。具体的Channel()函数源代码如下:
public fun <E> Channel(
capacity: Int = RENDEZVOUS,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
when (capacity) {
// 在工厂函数 Channel(...) 中创建没有缓冲区的通道
// 若缓冲区溢出的模式不是SUSPEND,创建缓冲区容量为1的通道
RENDEZVOUS -> {
if (onBufferOverflow == BufferOverflow.SUSPEND)
BufferedChannel(RENDEZVOUS, onUndeliveredElement)
else
ConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement)
}
// 在工厂函数 Channel(...) 中合并数据,缓冲区容量为1,总是使用新值替换旧值
CONFLATED -> {
require(onBufferOverflow == BufferOverflow.SUSPEND) {
"CONFLATED capacity cannot be used with non-default onBufferOverflow"
}
ConflatedBufferedChannel(1, BufferOverflow.DROP_OLDEST, onUndeliveredElement)
}
// 在工厂函数 Channel(...) 中创建具有无限容量缓冲区的通道
UNLIMITED -> BufferedChannel(UNLIMITED, onUndeliveredElement)
// 在工厂函数 Channel(...) 中创建具有默认缓冲容量的缓冲通道
// 缓冲区溢出模式为SUSPEND时,默认设置缓冲区的大小为64
// 否则,设置缓冲区的大小为1
BUFFERED -> {
if (onBufferOverflow == BufferOverflow.SUSPEND) BufferedChannel(CHANNEL_DEFAULT_CAPACITY, onUndeliveredElement)
else ConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement)
}
else -> {
if (onBufferOverflow === BufferOverflow.SUSPEND) BufferedChannel(capacity, onUndeliveredElement)
else ConflatedBufferedChannel(capacity, onBufferOverflow, onUndeliveredElement)
}
}
缓冲模式的定义:
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {
public companion object Factory {
public const val UNLIMITED: Int = Int.MAX_VALUE
public const val RENDEZVOUS: Int = 0
public const val CONFLATED: Int = -1
public const val BUFFERED: Int = -2
// 仅内部使用,不能用于Channel(...)函数中
internal const val OPTIONAL_CHANNEL = -3
// 配置默认缓冲容量属性的键名称
public const val DEFAULT_BUFFER_PROPERTY_NAME: String = "kotlinx.coroutines.channels.defaultBuffer"
// 默认缓冲容量属性
internal val CHANNEL_DEFAULT_CAPACITY = systemProp(DEFAULT_BUFFER_PROPERTY_NAME,64, 1, UNLIMITED - 1)
}
}
溢出模式的定义:
public enum class BufferOverflow {
// 缓冲区溢出时挂起
SUSPEND,
// 在溢出时删除缓冲区中最早的值,将新值添加到缓冲区,无需挂起
DROP_OLDEST,
// 在缓冲区溢出时删除当前添加到缓冲区的最新值(以便缓冲区内容保持不变),无需挂起
DROP_LATEST
}
示例1:缓冲区大小为0,溢出模式为BufferOverflow.SUSPEND:
val scope = CoroutineScope(Dispatchers.Default)
val channel = Channel<Int>(Channel.RENDEZVOUS, BufferOverflow.SUSPEND)
fun main() {
scope.launch {
for (element in 0 until 2) {
delay(1000)
println("send element = $element")
channel.send(element)
}
channel.close()
}
scope.launch {
delay(2000)
channel.consumeEach { println("receive element = $it") }
}
runBlocking { delay(10000) }
}
// 输出
send element = 0
receive element = 0
send element = 1
receive element = 1
这里为了方便输出,我们使channel接收数据的速度总是比发送数据的速度慢。从输出结果我们可以看出,当receiver()未执行完成时,send()总是会挂起。
示例2:缓冲区大小为1,溢出模式为BufferOverflow.DROP_OLDEST:
val scope = CoroutineScope(Dispatchers.Default)
val channel = Channel<Int>(Channel.CONFLATED)
fun main() {
scope.launch {
for (element in 0 until 3) {
println("send element = $element")
channel.send(element)
}
channel.close()
}
scope.launch {
delay(1000)
channel.consumeEach { println("receive element = $it") }
}
runBlocking { delay(5000) }
}
// 输出
send element = 0
send element = 1
send element = 2
receive element = 2
从输出结果我们可以看到,在send()所在的协程中发送了3条数据,但最终只有最后一条数据被接收了。由于这里的组合情况比较多,就不一一列举了。但只要我们掌握了缓冲区容量模式和溢出模式的定义,运用起来就比较好理解了。
4.使用扩展函数produce()创建一个生产者通道
public fun <E> CoroutineScope.produce(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> =
produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block)
produce()是CoroutineScope的一个扩展函数,它的作用是创建一个生产者通道,返回一个消费者通道。举个例子,比如说某商家在做线上活动免费发放10部遥遥领先手机来回馈1000个粉丝,活动开始后1000粉丝在线抢,那么我们就可以使用produce()函数来完成。
val scope = CoroutineScope(Dispatchers.IO)
val mutex = Mutex()
fun main() {
val receiveChannel = scope.produce {
for (element in 0 until 10) {
println("send element = $element")
channel.send(element)
}
}
repeat(100) {
scope.launch {
mutex.withLock {
if (receiveChannel.isClosedForReceive) return@launch
println("${Thread.currentThread()}: receiver element = ${receiveChannel.receiveCatching().getOrNull()}")
}
}
}
runBlocking { delay(5000) }
}
// 输出
send element = 0
send element = 1
Thread[DefaultDispatcher-worker-3,5,main]: receiver element = 0
Thread[DefaultDispatcher-worker-1,5,main]: receiver element = 1
...
通道本身就是协程安全的,这里为了取消打印无效的值,所以加了Mutex锁。produce()方法内部创建了ProducerCoroutine类型的Job:
private class ProducerCoroutine<E>(
parentContext: CoroutineContext, channel: Channel<E>
) : ChannelCoroutine<E>(parentContext, channel, true, active = true), ProducerScope<E> {
override val isActive: Boolean
get() = super.isActive
override fun onCompleted(value: Unit) {
_channel.close()
}
override fun onCancelled(cause: Throwable, handled: Boolean) {
val processed = _channel.close(cause)
if (!processed && !handled) handleCoroutineException(context, cause)
}
}
在协程完成或取消时会自动关闭通道,无需手动关闭。
5.使用扩展函数actor()创建一个消费者通道
public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
):
actor()同样是CoroutineScope的一个扩展函数,它的作用是创建一个消费者通道,返回一个生产者通道。
val scope = CoroutineScope(Dispatchers.IO)
fun main() {
val sendChannel = scope.actor<Int> {
for (element in channel) {
println("receiver element = $element")
}
}
scope.launch {
repeat(3) {
sendChannel.send(it)
}
}
scope.launch {
repeat(3) {
sendChannel.send(it)
}
}
runBlocking { delay(5000) }
}
// 输出
receiver element = 0
receiver element = 0
receiver element = 1
receiver element = 1
...
6.通道是公平的
发送和接收操作是公平的并且尊重调用它们的多个协程。它们遵守先进先出原则。如下代码示例:
val scope = CoroutineScope(Dispatchers.IO)
fun main() {
val channel = Channel<Int>()
scope.launch {
var element = 0
while (true) {
delay(100)
channel.send(element++)
}
}
repeat(10) {
scope.launch {
println("${Thread.currentThread()}: receiver element = ${channel.receive()}")
}
}
runBlocking { delay(10000) }
}
// 输出
Thread[DefaultDispatcher-worker-3,5,main]: receiver element = 0
Thread[DefaultDispatcher-worker-2,5,main]: receiver element = 1
Thread[DefaultDispatcher-worker-6,5,main]: receiver element = 2
Thread[DefaultDispatcher-worker-4,5,main]: receiver element = 3
Thread[DefaultDispatcher-worker-5,5,main]: receiver element = 4
Thread[DefaultDispatcher-worker-9,5,main]: receiver element = 5
Thread[DefaultDispatcher-worker-7,5,main]: receiver element = 6
...
尽管我们开启了10个不同的协程来接收生产者发送的数据,但是我们每个协程接收到的数据总是互斥的。
7.不互斥的通道BroadcastChannel
BroadcastChannel现在已经被标记废弃了,取而代之的是SharedFlow和StateFlow。
@Deprecated(level = DeprecationLevel.WARNING, message =
"BroadcastChannel is deprecated in the favour of SharedFlow and StateFlow,
and is no longer supported")
public fun <E> BroadcastChannel(capacity: Int): BroadcastChannel<E>
它的用法也比较简单,创建BroadcastChannel的方法与创建普通的Channel几乎没有区别:
val broadcastChannel = BroadcastChannel<Int>(5)
如果需要订阅功能,那么只需要调用如下方法:
val receiverChannel = broadcastChannel.openSubscription()
val scope = CoroutineScope(Dispatchers.IO)
fun main() {
val broadcastChannel = BroadcastChannel<Int>(5)
scope.launch {
delay(100)
arrayListOf(1, 2).onEach { broadcastChannel.send(it) }
}
repeat(2) {
scope.launch {
val receiverChannel = broadcastChannel.openSubscription()
for (element in receiverChannel) {
println("$coroutineContext : element = $element")
}
}
}
runBlocking { delay(1000) }
}
// 输出
[StandaloneCoroutine{Active}@602be23e, Dispatchers.IO] : element = 1
[StandaloneCoroutine{Active}@5e2fa749, Dispatchers.IO] : element = 1
[StandaloneCoroutine{Active}@602be23e, Dispatchers.IO] : element = 2
[StandaloneCoroutine{Active}@5e2fa749, Dispatchers.IO] : element = 2
从例子中我们可以看出广播时每一个接收端都可以读取每一个元素。这个例子中有个细节需要我们注意下,如果把发送端delay(100)去掉,你可能会发现什么都不会输出,或者部分元素接收不到。这是因为如果BroadcastChannel在发送数据时没有订阅者,那么这条数据就会被直接丢弃。除了直接创建以外,我们也可以用前面定义的普通Channel进行转换,如下代码示例:
val channel = Channel<Int>()
val broadcastChannel = channel.broadcast(5)
8.close()方法
public interface SendChannel<in E> {
...
public fun close(cause: Throwable? = null): Boolean
...
}
以BufferedChannel实现为例:
override fun close(cause: Throwable?): Boolean =
closeOrCancelImpl(cause, cancel = false)
closeOrCancelImpl()方法中会调用markClosed()方法:
// 将此通道标记为已关闭。如果 cancelImpl 已调用,并且此通道标记为
// CLOSE_STATUS_CANCELLATION_STARTED,此函数将通道标记为已取消。
// 所有注意到此通道处于关闭状态的操作都必须通过 completeCloseOrCancel完成关闭。
private fun markClosed(): Unit =
sendersAndCloseStatus.update { cur ->
when (cur.sendersCloseStatus) {
CLOSE_STATUS_ACTIVE ->
// 关闭通道
constructSendersAndCloseStatus(cur.sendersCounter, CLOSE_STATUS_CLOSED)
CLOSE_STATUS_CANCELLATION_STARTED ->
// 取消通道
constructSendersAndCloseStatus(cur.sendersCounter, CLOSE_STATUS_CANCELLED)
else -> return // the channel is already marked as closed or cancelled.
}
}
close()方法内部通过一个原子的Long类型变量来标记通道处于关闭状态。
9.Channel的数据结构
private val sendSegment: AtomicRef<ChannelSegment<E>>
private val receiveSegment: AtomicRef<ChannelSegment<E>>
private val bufferEndSegment: AtomicRef<ChannelSegment<E>>
在BufferedChannel类中定义了3个不同的属性,数据的发送、接收和缓冲都是通过ChannelSegment来处理的。ChannelSegment则是一个线程安全的双向链表结构,其继承关系如下:
--ConcurrentLinkedListNode
--Segment
--ChannelSegment
nternal abstract class ConcurrentLinkedListNode<N : ConcurrentLinkedListNode<N>>(prev: N?) {
private val _next = atomic<Any?>(null)
private val _prev = atomic(prev)
private val nextOrClosed get() = _next.value
...
}
总结:
尽管Channel大多数的使用场景都被StateFlow和SharedFlow所取代,但是作为协程的一个重要知识点,我们还是有必要去了解一下。