Kotlin之生产者消费者模型Channel的使用

前言:

前几篇文章我们介绍了协程的基础知识、上下文、协程的取消和异常:
第一节: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所取代,但是作为协程的一个重要知识点,我们还是有必要去了解一下。

  • 35
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值