You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

19 KiB

31 | 图解Channel如何理解它的CSP通信模型

你好我是朱涛。今天我们来分析Channel的源码。

Kotlin的Channel是一个非常重要的组件在它出现之前协程之间很难进行通信有了它以后协程之间的通信就轻而易举了。在第22讲当中我们甚至还借助Channel实现的Actor做到了并发安全。

那么总的来说Channel是热的同时它还是一个线程安全的数据管道。而由于Channel具有线程安全的特性因此它最常见的用法就是建立CSP通信模型Communicating Sequential Processes

不过你可能会觉得CSP太抽象了不好理解但其实这个通信模型我们在第22讲里就接触过了。当时我们虽然是通过Actor来实现的但却是把它当作CSP在用它们两者的差异其实很小。

关于CSP的理论,它的精确定义其实比较复杂,不过它的核心理念用一句话就可以概括:不要共享内存来通信;而是要用通信来共享内存Dont communicate by sharing memory; share memory by communicating

可是我们为什么可以通过Channel实现CSP通信模型呢这背后的技术细节则需要我们通过源码来发掘了。

Channel背后的数据结构

为了研究Channel的源代码我们仍然是以一个简单的Demo为例来跟踪它的代码执行流程。

// 代码段1

fun main()  {
    val scope = CoroutineScope(Job() + mySingleDispatcher)
    // 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("Receive ${result}")
        }
    }

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

/*
输出结果:
end
Receive 0
Send: 0
Send: 1
Receive 1
Receive 2
Send: 2
*/

以上代码主要分为三个部分分别是Channel创建、发送数据、接收数据。

我们先来分析注释1处的Channel创建逻辑。我们都知道Channel其实是一个接口它是通过组合SendChannel、ReceiveChannel得来的。而注释1处调用的Channel(),其实是一个普通的顶层函数,只是它发挥的作用是构造函数,因此它的首字母是大写的这跟我们上节课分析的CoroutineScope、Job也是类似的。

// 代码段2

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 -> {
            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)
        }
    }

然后,从上面的代码里,我们可以看到,Channel()方法的核心逻辑就是一个when表达式它根据传入的参数会创建不同类型的Channel实例包括了RendezvousChannel、ArrayChannel、ConflatedChannel、LinkedListChannel。而这些实现类都有一个共同的父类AbstractChannel

// 代码段3

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> {}
}

可以看到AbstractChannel其实是AbstractSendChannel的内部类同时它也是AbstractSendChannel的子类。而Channel当中的核心逻辑都是依靠AbstractSendChannel当中的 LockFreeLinkedListHead 实现的。我们接着来看下它的源代码:

// 代码段4

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

public actual open class LockFreeLinkedListNode {
    // 1
    private val _next = atomic<Any>(this)
    private val _prev = atomic(this)
    private val _removedRef = atomic<Removed?>(null)
}

可见LockFreeLinkedListHead其实继承自 LockFreeLinkedListNode而LockFreeLinkedListNode则是实现Channel核心功能的关键数据结构。整个数据结构的核心思想来自于2004年的一篇论文《Lock-Free and Practical Doubly Linked List-Based Deques Using Single-Word Compare-and-Swap》。如果你对其中的原理感兴趣,可以去看看这篇论文。这里,为了不偏离主题,我们只分析它的核心思想。

LockFreeLinkedListNode我们可以将其区分开来看待即LockFree和LinkedList。

第一个部分:LockFree,它是通过CASCompare And Swap的思想来实现的比如JDK提供的java.util.concurrent.atomic。这一点我们从上面注释1的atomic也可以看出来。

第二个部分:LinkedList这说明LockFreeLinkedList本质上还是一个链表。简单来说它其实是一个循环双向链表而LockFreeLinkedListHead其实是一个哨兵节点,如果你熟悉链表这个数据结构,也可以将其看作是链表当中的虚拟头结点这个节点本身不会用于存储任何数据它的next指针会指向整个链表的头节点而它的prev指针会指向整个链表的尾节点

为了方便你理解,我画了一张图描述这个链表的结构:

请看图片左边的部分,当链表为空的时候LockFreeLinkedListHead的next指针和prev指针都是指向自身的。这也就意味着这个Head节点是不会存储数据同时也是不会被删除的。

然后再看图片右边的部分,当链表有2个元素的时候这时LockFreeLinkedListHead节点的next指针才是第一个节点而Head的prev指针则是指向尾结点。

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

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

在弄清楚LockFreeLinkedList这个数据结构以后Channel后续的源码分析就很简单了。让我们来分别分析一下Channel的send()、receive()的流程。

发送和接收的流程

我们回过头来看代码段1当中的逻辑我们分别启动了两个协程在这两个协程中我们分别发送了三次数据也接收了三次数据。程序首先会执行send()由于Channel在默认情况下容量是0所以send()首先会被挂起。让我们来看看这部分的逻辑:

// 代码段5

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_FAILE
        // 省略
    }
}

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 -> {
                    // 5
                    cont.removeOnCancellation(send)
                    return@sc
                }
                enqueueResult is Closed<*> -> {
                }
                enqueueResult === ENQUEUE_FAILED -> {} 
                enqueueResult is Receive<*> -> {} 
                else -> error("enqueueSend returned $enqueueResult")
            }
        }
        // 省略
    }
}

上面的挂起函数send()分为两个部分:

  • 注释1尝试向Channel发送数据如果这时候Channel已经有了消费者那么if就会为truesend()方法就会return。不过按照代码段1的逻辑首次调用send()的时候Channel还不存在消费者因此在注释3处尝试从LockFreeLinkedList取出消费者是不可能的。所以程序会继续执行注释2处的逻辑。
  • 注释2会调用挂起函数sendSuspend()它是由高阶函数suspendCancellableCoroutineReusable{} 实现的。我们看它的名字就能知道它跟suspendCancellableCoroutine{} 是类似的(如果你有些忘了,可以回过头去看看加餐五。另外请留意下这个方法的注释4它会将发送的元素封装成SendElement对象然后调用enqueueSend()方法将其添加到LockFreeLinkedList这个队列的末尾。如果enqueueSend()执行成功了就会执行注释5注册一个回调用于将SendElement从队列中移除掉。

如果你足够细心的话你会发现这整个流程并没有涉及到resume的调用因此这也意味着sendSuspend()会一直被挂起而这就意味着send()会一直被挂起!那么,问题来了,send()会在什么时候被恢复

答案当然是:receive()被调用的时候

// 代码段6

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()
    }
}

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

internal fun <T> DispatchedTask<T>.dispatch(mode: Int) {
    // 省略
    if (!undispatched && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) {

        val dispatcher = delegate.dispatcher
        val context = delegate.context
        if (dispatcher.isDispatchNeeded(context)) {
            // 6
            dispatcher.dispatch(context, this)
        } else {
            resumeUnconfined()
        }
    } else {
        // 省略
    }
}

可以看到挂起函数receive()的逻辑跟代码段5当中的send()是类似的。

  • 注释1尝试从LockFree队列当中找出是否有正在被挂起的发送方。具体的逻辑在注释3处它会从队首开始遍历寻找Send节点。
  • 接着上面的代码段1的案例分析此时我们一定是可以从队列中找到一个Send节点的因此程序会继续执行注释4处的代码。
  • 注释4completeResumeSend()它最终会调用注释5处的dispatch(mode)而dispatch(mode)其实就是DispatchedTask的dispatch()是不是觉得很熟悉这个DispatchedTask其实就是我们在第29讲当中分析过的DispatchedTask这里的dispatch()就是协程体当中的代码在线程执行的时机。最终它会执行在Java的Executor之上。至此我们之前被挂起的send()方法,其实就算是恢复了。

另外你可以再留意上面的注释2当LockFree队列当中没有正在挂起的发送方时它会执行receiveSuspend()而receiveSuspend()也同样会被挂起:

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
        }

        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
        }
    }
}

所以这里的逻辑其实跟之前的sendSuspend()是类似的。首先它会封装一个ReceiveElement对象并且将其添加到LockFree队列的末尾如果添加成功的话这个receiveSuspend就会继续挂起这就意味着receive()也会被挂起。而receive()被恢复的时机其实就对应了代码段5当中注释1的代码offerInternal(element)。

至此Channel的发送和接收流程我们就都已经分析完了。按照惯例我们还是通过一个视频来回顾代码的整体执行流程

小结

通过这节课我们知道Channel其实是一个线程安全的管道。它最常见的用法就是实现CSP通信模型。它的核心理念是不要共享内存来通信;而是要用通信来共享内存。而Channel之所以可以用来实现CSP通信模型主要还是因为它底层用到的数据结构LockFreeLinkedList。

LockFreeLinkedList虽然是一个循环双向链表但在Channel的源码中它会被当做先进先出的单向队列,它只在队列末尾插入节点,而遍历则只正向遍历。

还有Channel的send()它会分为两种情况一种是当前的LockFree队列当中已经有被挂起的接收方这时候send()会恢复Receive节点的执行并且将数据发送给对方。第二种情况是当前队列当中没有被挂起的接收方这时候send()就会被挂起而被发送的数据会被封装成SendElement对象插入到队列的末尾等待被下次的receive()恢复执行。

而Channel的receive()也是分为两种情况一种是当前的LockFree队列当中已经存在被挂起的发送方这时候receive()会恢复Send节点的执行并且取出Send节点当中带过来的数据。第二种情况是当前队列没有被挂起的发送方这时候receive()就会被挂起同时它也会被封装成一个ReceiveElement对象插入到队列的末尾等待被下次的send()恢复执行。

其实Kotlin推崇CSP模型进行并发的原因还有很多比如门槛低、可读性高、扩展性好还有一点是会被很多人提到的不容易发生死锁。

不过这里需要特别注意的是CSP场景下的并发模型并非不可能发生死锁在一些特殊场景下它也是可能发生死锁的比如通信死锁Communication Deadlock。因此CSP也并不是解决所有并发问题的万能解药我们还是要具体问题具体分析。

思考题

在课程的开头我们分析了Channel一共有四种实现方式RendezvousChannel、ArrayChannel、ConflatedChannel、LinkedListChannel请问你能结合今天学习的知识分析LinkedListChannel的原理吗

internal open class LinkedListChannel<E>(onUndeliveredElement: OnUndeliveredElement<E>?) : AbstractChannel<E>(onUndeliveredElement) {
    protected final override val isBufferAlwaysEmpty: Boolean get() = true
    protected final override val isBufferEmpty: Boolean get() = true
    protected final override val isBufferAlwaysFull: Boolean get() = false
    protected final override val isBufferFull: Boolean get() = false

    protected override fun offerInternal(element: E): Any {
        while (true) {
            val result = super.offerInternal(element)
            when {
                result === OFFER_SUCCESS -> return OFFER_SUCCESS
                result === OFFER_FAILED -> { // try to buffer
                    when (val sendResult = sendBuffered(element)) {
                        null -> return OFFER_SUCCESS
                        is Closed<*> -> return sendResult
                    }
                    // otherwise there was receiver in queue, retry super.offerInternal
                }
                result is Closed<*> -> return result
                else -> error("Invalid offerInternal result $result")
            }
        }
    }

    protected override fun offerSelectInternal(element: E, select: SelectInstance<*>): Any {
        while (true) {
            val result = if (hasReceiveOrClosed)
                super.offerSelectInternal(element, select) else
                (select.performAtomicTrySelect(describeSendBuffered(element)) ?: OFFER_SUCCESS)
            when {
                result === ALREADY_SELECTED -> return ALREADY_SELECTED
                result === OFFER_SUCCESS -> return OFFER_SUCCESS
                result === OFFER_FAILED -> {} // retry
                result === RETRY_ATOMIC -> {} // retry
                result is Closed<*> -> return result
                else -> error("Invalid result $result")
            }
        }
    }
}