Kotlin中Channel的使用

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情

什么是Channel

Channel API是用来在多个协程之间进行通信的,并且它是并发安全的。它的概念有点与BlockQueue相似,都遵循先进先出的规则,差别就在于Channel使用挂起的概念替代了BlockQueque中的阻塞。使用它我们可以很轻易的构建一个生产者消费者模型。并且Channel支持任意数量的生产者和消费者

channel_mimo.webp

从源码我们可以看出Channel主要实现了两个接口

public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {}

interface SendChannel<in E> {
    suspend fun send(element: E)
    public fun trySend(element: E): ChannelResult<Unit>
    fun close(): Boolean
    //...
}

interface ReceiveChannel<out E> {
    suspend fun receive(): E
    public fun tryReceive(): ChannelResult<E>
    fun cancel(cause: CancellationException? = null)
    // ...
}
  • SendChannel: 用于添加元素到通道中和关闭通道;
  • ReceiveChannel:主要用于接收通道中的元素

你会发现SendChannel中的send()和ReceiveChannel中的receive方法都是挂起函数,为什么会怎么设计,在通道中如果存储元素的数量达到了我们设置的通道存储大小的时候,再通过send()方法往通道中发送数据,就会挂起,直至通道有空闲空间是才会将挂起的发送动作恢复。同理,如果我们的通道中没有可用的元素时,这个时候我们通过receive方法去接收数据,就会发现此操作将会被挂起,直到通道中存在可用元素为止。

如果我们需要在非挂起函数中去接收和发送数据,我们可以使用trySendtryReceive,这两个操作都会立即返回一个ChannelResult,结果中会包含此次操作的的结果以及数据,但是这两个操作只能使用在容量有限的通道上。

Channel的使用

下面我们通过构建一个简单的消费者和生产者模型了解以下Channel的使用

suspend fun main(): Unit = runBlocking {
        val channel = Channel<String>()
	//生产者协程
         launch { 
            channel.send("Hello World!")
         }
        //消费者协程
        launch {
            val received = channel.receive()
            println(received)
    	}
    }
}

上面这种创建Channel的方式,在我们使用完通道之后很容易忘记一个close操作,特别是如果其中一个生产者协程应为某些情况发生异常,停止了生产,那么消费者协程会一直挂起等待生产者生产完成进行消费。所以我们可以使用协程的一个扩展方法produce,当协程发生异常,或者是协程完成时,它会自动去调用close方法,并且它会返回一个ReceiveChannel,下面我们就来看看怎么使用

runBlocking {
    val channel = produce {
        listOf("apple","banana","orange").forEach {
             send(it)
        }
    }
	for (element in channel){
         print(element)
     }
}

Channel有哪些

我们在使用Channel函数在创建通道时,我们会指定通道的容量大小,然后会根据容量创建不同类型的通道

public fun <E> Channel(
    capacity: Int = RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
    when (capacity) {
        RENDEZVOUS -> {
            if (onBufferOverflow == BufferOverflow.SUSPEND)
                RendezvousChannel(onUndeliveredElement) 
            else
                ArrayChannel(1, onBufferOverflow, onUndeliveredElement)
        }
        CONFLATED -> {
		   require(onBufferOverflow == BufferOverflow.SUSPEND) {
                "CONFLATED capacity cannot be used with non-default onBufferOverflow"
            }
            ConflatedChannel(onUndeliveredElement)
        }
        UNLIMITED -> LinkedListChannel(onUndeliveredElement)
        BUFFERED -> ArrayChannel( 
            if (onBufferOverflow == BufferOverflow.SUSPEND)
            	CHANNEL_DEFAULT_CAPACITY
            else 1,
            onBufferOverflow, onUndeliveredElement
        )
        else -> {
            if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST)
                ConflatedChannel(onUndeliveredElement) 
            else
                ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement)
        }
    }

可以从以上源码看出我们的通道主要分为4种类型

  • RENDEZVOUS :默认容量为0,且生产者和消费者只有在相遇时才能进行数据的交换
  • CONFLATED :容量大小为1,且每个新元素会替换前一个元素
  • UNLIMITED: 无限容量缓冲区且send永不挂起的通道。
  • BUFFERED : 默认容量为64,且在溢出时挂起的通道,可以通过设置JVM的 DEFAULT_BUFFER_PROPERTY_NAME来覆盖它

我们从Channel源码看出,Channel在创建时还会指定缓冲区溢出时的策略

扫描二维码关注公众号,回复: 14338985 查看本文章
public enum class BufferOverflow {
    //缓冲区满时,将操作进行挂起,等待缓冲区有空间
    SUSPEND,
    //删除旧值
    DROP_OLDEST,
    //将即将要添加进缓冲区的值删除
    DROP_LATEST
}

Channel函数还有一个可选参数onUndeliveredElement,接收一个Lambda在元素被发送且未被消费时调用,我们通常使用它来关闭一些该通道发送的资源。

在Channel内部结构种维护的缓冲区结构除了ArrayChannel内部自己维护了一个数组作为缓冲区,其余的都是使用AbstractSendChannel的链表作为缓冲区

那么我们将两个管道的内容合并成一个呢

fun <T> CoroutineScope.fanIn(
    channels: List<ReceiveChannel<T>>
): ReceiveChannel<T> = produce {
    for (channel in channels) {
        launch {
            for (elem in channel) {
                send(elem)
            }
        }
    }
}

猜你喜欢

转载自juejin.im/post/7112032818972065799