【Kotlin回顾】23.Kotlin协程—Channel流程


在分析Kotlin协程—协程的并发中用Actor也实现了并发安全,而Actor是本质是基于Channel管消息实现的,所以Channel是一个线程安全的数据管道,根据Channel的这个安全特性它最常见的用法是建立CSP通信模型。

CSP通信模型(Communicating Sequential Processes),是一种形式化语言,用于描述并发系统中的交互模式。CSP 允许根据独立运行的组件进程来描述系统,并且仅通过消息传递通信相互交互。

1.为什么可以通过Channel实现CSP通信模型?

分析Channel实现CSP通信模型首先要从Channel的数据结构入手,先来看一段demo

fun main() = runBlocking {
    val scope = CoroutineScope(Job())
    // 1,创建管道
    val channel = Channel<Int>()

    scope.launch {
        // 2,在一个单独的协程当中发送管道消息
        repeat(3) {
            channel.send(it)
            println("send: $it")
        }

        channel.close()
    }

    scope.launch {
        // 3,在一个单独的协程当中接收管道消息
        repeat(3) {
            val result = channel.receive()
            println("result $result")
        }
    }

    println("end")
    Thread.sleep(2000000L)
}

//输出结果:
//end
//result 0
//send: 0
//result 1
//send: 1
//result 2
//send: 2

代码分为三个部分,创建Channel、通过channel发送数据,通过channel接收数据。

2.Channel的数据结构

在注释1这里创建了一个Channel。Channel是一个接口,同时它还实现了SendChannelReceiveChannel,但是注释1中创建时调用的Channel其实是一个普通的顶层函数,它的作用是一个构造函数,所以首字母是大写的,在前面的Kotlin协程—CoroutineScope是如何管理协程的也是提到过的。

public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {
    ...
    
	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) // 忽略onBufferOverflow:它有缓冲区,但它永远不会溢出
        BUFFERED -> ArrayChannel( // 使用默认容量与SUSPEND
            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)
        }
    }
}

通过Channel函数中可以发现它的核心逻辑就是一个when表达式,根据传入的参数创建不同的Channel实例:RendezvousChannelArrayChannelConflatedChannelLinkedListChannel,并且这几个Channel的父类都是AbstractChannelAbstractChannel的父类又是AbstractSendChannel

/**
 * 它是所有发送通道实现的基类。
 */
internal abstract class AbstractSendChannel<E>(
    @JvmField protected val onUndeliveredElement: OnUndeliveredElement<E>?
) : SendChannel<E> {

    protected val queue = LockFreeLinkedListHead()
    
    ...

	//抽象的发送/接收通道。它是所有通道实现的基类。
    internal abstract class AbstractChannel<E>(
    onUndeliveredElement: OnUndeliveredElement<E>?
) : AbstractSendChannel<E>(onUndeliveredElement), Channel<E> {
        
}
    
}

AbstractSendChannel类中还有一个变量val queue = LockFreeLinkedListHead(),Channel的核心逻辑就是依靠它实现的。

public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() {
    public actual val isEmpty: Boolean get() = next === this
}

public actual open class LockFreeLinkedListNode {
    private val _next = atomic<Any>(this) // Node | Removed | OpDescriptor
    private val _prev = atomic(this) // Node to the left (cannot be marked as removed)
    private val _removedRef = atomic<Removed?>(null) // lazily cached removed ref to this
}

LockFreeLinkedListHead 其实继承自 LockFreeLinkedListNode,而 LockFreeLinkedListNode 则是实现 Channel 核心功能的关键数据结构。

看到LinkedList基本可以确定这是一个链表,而LockFreeLinkedListHead是一个循环双向链表,LockFreeLinkedListHead是一个哨兵节点。这个节点本身不会用于存储任何数据,它的 next 指针会指向整个链表的头节点,而它的 prev 指针会指向整个链表的尾节点。

哨兵节点是为了简化处理链表边界条件而引入的附加链表节点,哨兵节点通常位于链表头部它的值没有任何意义,在一个有哨兵节点的链表中,从第二个节点开始才真正保存有意义的信息。

这里先用两张图来分别标识空链表和有元素的链表的结构

链表为空的时候LockFreeLinkedListHead的next指针和prev指针都指向自身的,这就意味着这个节点是不会存储数据的,也是不会被删除的。

链表中有2个或两个以上的元素时LockFreeLinkedListHead的next指针才是第一个节点,prev则指向尾结点。

寻常的循环双向链表是可以在首尾添加元素的,同时也支持“正向遍历、逆向遍历”的。而Channel内部的数据结构只能在末尾添加,遍历顺序则是从队头开始,这样的设计就让他的行为变成了先进先出单向队列的同时还实现了队尾添加操作,只需要O(1)的时间复杂度。

可以说,正是因为 LockFreeLinkedList 这个数据结构,才能使用 Channel 实现 CSP 通信模型。

3.发送和接收的流程

前面的demo中启动了两个协程,在这两个协程中发送了三次数据和接收了三次数据,程序首先会执行send(),但是由于Channel默认情况下容量是0,所以send()首先会被挂起。

public final override suspend fun send(element: E) {
    //1
    if (offerInternal(element) === OFFER_SUCCESS) return
    //2
    return sendSuspend(element)
}

/**
 * 尝试将元素添加到缓冲区或队列接收方
 */
protected open fun offerInternal(element: E): Any {
    while (true) {
        //3
        val receive = takeFirstReceiveOrPeekClosed() ?: return OFFER_FAILED
        val token = receive.tryResumeReceive(element, null)
        if (token != null) {
            assert { token === RESUME_TOKEN }
            receive.completeResumeReceive(element)
            return receive.offerResult
        }
    }
}

private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont ->
        loop@ while (true) {
            if (isFullImpl) {
                //4
                val send = if (onUndeliveredElement == null)
                    SendElement(element, cont) else
                    SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement)
                val enqueueResult = enqueueSend(send)
                when {
                    enqueueResult == null -> { // enqueued successfully
                        //5
                        cont.removeOnCancellation(send)
                        return@sc
                    }
                    enqueueResult is Closed<*> -> {
                        cont.helpCloseAndResumeWithSendException(element, enqueueResult)
                        return@sc
                    }
                    enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead
                    enqueueResult is Receive<*> -> {} // try to offer instead
                    else -> error("enqueueSend returned $enqueueResult")
                }
            }
            // hm... receiver is waiting or buffer is not full. try to offer
            val offerResult = offerInternal(element)
            when {
                offerResult === OFFER_SUCCESS -> {
                    cont.resume(Unit)
                    return@sc
                }
                offerResult === OFFER_FAILED -> continue@loop
                offerResult is Closed<*> -> {
                    cont.helpCloseAndResumeWithSendException(element, offerResult)
                    return@sc
                }
                else -> error("offerInternal returned $offerResult")
            }
        }
    }

  • 注释1:尝试向Channel发送数据,在有消费这的情况下if判断就是true,那么此时就会return,但是如果是第一次调用Channel就不会有消费者,所以offerInternal函数中想要从队列中取出消费者是不可能的(注释3),所以第一次调用就进入了sendSuspend函数。
  • 注释2:这个挂起函数是由高阶函数suspendCancellableCoroutineReusable实现的,这个高阶函数的作用就是暴露挂起函数的Continuation对象。
  • 注释4:这里的作用就是把发送的元素封装成SendElement对象,然后调用enqueueSend()方法将其添加到LockFreeLinkedList队列的末尾,添加成功则进入注释5开始执行。
  • 注释5:这里就是将SendElement从队列中删除。

到这里应该会有一个疑问:send()函数一直是被挂起的啊,它会在什么时候恢复呢?

答案就是接收数据的时候:receive()

public final override suspend fun receive(): E {
    // 1
    val result = pollInternal()
 
    @Suppress("UNCHECKED_CAST")
    if (result !== POLL_FAILED && result !is Closed<*>) return result as E
    // 2
    return receiveSuspend(RECEIVE_THROWS_ON_CLOSE)
}

protected open fun pollInternal(): Any? {
    while (true) {
        //3
        val send = takeFirstSendOrPeekClosed() ?: return POLL_FAILED
        val token = send.tryResumeSend(null)
        if (token != null) {
            assert { token === RESUME_TOKEN }
            //4
            send.completeResumeSend()
            return send.pollResult
        }
        
        send.undeliveredElement()
    }
}

private suspend fun <R> receiveSuspend(receiveMode: Int): R = suspendCancellableCoroutineReusable sc@ { cont ->
   
    val receive = if (onUndeliveredElement == null)
        ReceiveElement(cont as CancellableContinuation<Any?>, receiveMode) else
        ReceiveElementWithUndeliveredHandler(cont as CancellableContinuation<Any?>, receiveMode, onUndeliveredElement)
    while (true) {
        if (enqueueReceive(receive)) {
            removeReceiveOnCancel(cont, receive)
            return@sc
        }
        // hm... something is not right. try to poll
        val result = pollInternal()
        if (result is Closed<*>) {
            receive.resumeReceiveClosed(result)
            return@sc
        }
        if (result !== POLL_FAILED) {
            cont.resume(receive.resumeValue(result as E), receive.resumeOnCancellationFun(result as E))
            return@sc
        }
    }
}

  • 注释1:尝试从 LockFreeLinkedList 队列当中找出是否有正在被挂起的发送方;
  • 注释2:当 LockFreeLinkedList 队列当中没有正在挂起的发送方时,它会执行 receiveSuspend(),而 receiveSuspend() 也同样会被挂起,这个逻辑跟前面的sendSuspend()是类似的,先封装成receiveElement对象并添加队列的末尾,如果添加成功的话这个receiveSuspend()就会继续被挂起,这也就意味着receive()也会被挂起,而它的恢复时机则是在offerInternal()中;
  • 注释3:在send()流程中这一步是将发送的元素封装成SendElement对象,那么在receive()中这一步是取出封装好的SendElement对象,然后调用注释4;
  • 注释4:这里其实调用的是
override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN)

这里的cont其实是continuation,前面聊过,它是一个状态机,cont.completeResume(RESUME_TOKEN)会回调CancellableContinuationImpl中的completeResume

override fun completeResume(token: Any) {
    assert { token === RESUME_TOKEN }
    dispatchResume(resumeMode)
}

private fun dispatchResume(mode: Int) {
    if (tryResume()) return 
    dispatch(mode)
}

//DispatchedTask
internal fun <T> DispatchedTask<T>.dispatch(mode: Int) {
    assert { mode != MODE_UNINITIALIZED } // invalid mode value for this method
    val delegate = this.delegate
    val undispatched = mode == MODE_UNDISPATCHED
    if (!undispatched && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) {
        // dispatch directly using this instance's Runnable implementation
        val dispatcher = delegate.dispatcher
        val context = delegate.context
        if (dispatcher.isDispatchNeeded(context)) {
            dispatcher.dispatch(context, this)
        } else {
            resumeUnconfined()
        }
    } else {
        // delegate is coming from 3rd-party interceptor implementation (and does not support cancellation)
        // or undispatched mode was requested
        resume(delegate, undispatched)
    }
}

最终调用dispatch(mode),而 dispatch(mode) 其实就是 DispatchedTask 的 dispatch(),而这个dispatch()就是协程体当中的代码在线程执行的时机,最终,它会执行在 Java 的 Executor 之上。至此,之前被挂起的 send() 方法,就算是恢复了。

4.总结:

  • Channel是一个线程安全的管道,常见用法是实现CSP通信模型,它之所以可以用这种方式来实现CSP通信模型主要还是因为底层的数据结构是LockFreeLinkedList
  • LockFreeLinkedList是一个双向循环的链表但是在Channel源码中会被当做先进先出的单向队列,只是在队列末尾插入节点,且只会正向遍历;
  • Channel的发送分为两种情况,一种是当前队列中已经存在发送方,这时候send()会恢复 Receive 节点的执行,并将数据发送给对方;另一种情况是当前队列中不存在发送方,就是首次发送,则会挂起并将元素封装成SendElement后添加到LockFreeLinkedList队列的末尾,等待被receive()恢复执行;
  • Channel的接收分为两种情况,一种是当前队列中已经存在被挂起的发送方,这时候receive()会恢复Send节点的执行,并且取出Send节点带过来的数据 ; 另一种情况是当前队列中不存在被挂起的发送方,那么这时候receive()就会被挂起,同时会封装元素为ReceiveElement并添加到队列的末尾,等待被下一个send()恢复执行。

本文转自 (https://juejin.cn/post/7179594412459032631),如有侵权,请联系删除。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值