2024年HarmonyOS鸿蒙最新协程(22) Channel原理解析_xvpp002,带你全面掌握高级知识点

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

Send: 1
Receive 1
Receive 2
Send: 2
*/


在这里会发现输出结果是交替执行的,这是因为`Channel`的`send`和`receive`是挂起函数,而默认参数创建的`Channel`是没有缓存容量的,所以调用完`send`后,如果没有消费者来消费,就会挂起;同理`receive`也是如此,这些知识点我们在之前学习`Channel`文章时,已经说过这些特性了。  
 再结合挂起函数的本质,这种交替执行的输出结果,我相信都能明白。本篇文章,就来探索一下,`Channel`到底是如何实现的。  
 和我们之前分析的`CoroutineScope`、`Job`等类似,`Channel()`也是一个顶层函数充当构造函数使用的案例,该方法代码如下:



//顶层函数充当构造函数使用
public fun Channel(
//容量
capacity: Int = RENDEZVOUS,
//背压策略
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
//元素投递失败回调
onUndeliveredElement: ((E) -> Unit)? = null
): Channel =
when (capacity) {
//根据容量分类
RENDEZVOUS -> {
//默认参数下,所创建的Channel
if (onBufferOverflow == BufferOverflow.SUSPEND)
RendezvousChannel(onUndeliveredElement)
else
//背压策略是非挂起情况下的实现
ArrayChannel(1, onBufferOverflow, onUndeliveredElement)
}
CONFLATED -> {

ConflatedChannel(onUndeliveredElement)
}
UNLIMITED -> LinkedListChannel(onUndeliveredElement)
//容量为2,默认也是ArrayChannel
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`实例,比如`RendezvousChannel`、`ArrayChannel`等,我们等会以默认的`RendezvousChannel`为例来分析。  
 这里有个小知识点,就是`onUndeliveredElement`参数,这里使用函数类型,即符合Kotlin的语法规则,又不用创建多余接口。  
 但是`(E) -> Unit`这种函数类型是否会造成误解呢?因为毕竟丢失的元素可以用这个函数类型表示,那我再定义一个到达元素的回调呢,是不是也可以定义为(E) -> Unit。为了避免造成这种误解,我们看看是如何实现的,我们看看`RendezvousChannel`的定义:



internal open class RendezvousChannel(onUndeliveredElement: OnUndeliveredElement?) : AbstractChannel(onUndeliveredElement)


会发现这里参数类型居然是OnUndeliveredElement,这就很容易理解了。这里难道是定义了接口吗?我们查看一下:



internal typealias OnUndeliveredElement = (E) -> Unit


可以发现这里只是给类型起了一个别名,通过`typealias`可以给一些容易造成理解混乱的函数类型起个名字,这个小知识点,在实际业务中,还是蛮有用的。  
 回到主线,我们来分析`RendezvousChannel`的继承关系:



//该类继承至AbstractChannel
internal open class RendezvousChannel(onUndeliveredElement: OnUndeliveredElement?) :
AbstractChannel(onUndeliveredElement)



//继承至AbstractSendChannel类,实现Channel接口
internal abstract class AbstractChannel(
onUndeliveredElement: OnUndeliveredElement?
) : AbstractSendChannel(onUndeliveredElement), Channel



//实现SendChannel接口
internal abstract class AbstractSendChannel(
@JvmField protected val onUndeliveredElement: OnUndeliveredElement?
) : SendChannel



//Channel接口,继承至SendChannel和ReceiveChannel接口
public interface Channel : SendChannel, ReceiveChannel


乍一看,这里的接口和抽象类定义的有点复杂,但是我们稍微分析一下,就会发现这样定义挺合理:


* 首先就是一个最基础的问题,接口和抽象类的区别?  
 从面向对象解读来看,以及使用角度来分析,接口是倾向于约束公共的功能,或者给一个类添加额外的功能,某个类实现了接口,它就有了一些额外的能力行为。同时约束了该类,有这些功能。  
 比如这里的`SendChannel`接口,就表示一个管道发送方,所以它约束了一些统一操作:`send`、`trySend`等。  
 而抽象类,更多的是公共代码的抽取,或者一个抽象事务的基本实现。比如这里的`AbstractChannel<E>`就代表传递E类型的抽象管道实现,在里面实现了大多数的公共函数功能。
* 这里`Channel`接口,继承至`SendChannel`和`ReceiveChannel`,即把发送端和接收端给分开了,根据接口的定义,`Channel`就是具有发送端和接收端的管道。
* 这里`AbstractChannel`代表发送方的抽象实现或者公共实现,构造函数的参数可以接收发送失败的回调处理。


搞明白这几个抽象类,我们接下来就很好分析了。


### LockFreeLinkedList简析


首先是`AbstractChannel`,为什么发送端单独需要抽离出一个抽象类呢?这也是因为,发送端的逻辑比较复杂,同时它还也是Channel是线程安全的核心实现点。  
 在`AbstractChannel`中,有下面一个变量:



internal abstract class AbstractSendChannel(
@JvmField protected val onUndeliveredElement: OnUndeliveredElement?
) : SendChannel {
protected val queue = LockFreeLinkedListHead()


可以发现这是一个`queue`,即队列,同时它还是一个线程安全的队列,从`LockFreeLinkedList`就可以看出,它是一个没有使用锁`Lock`的`LinkedList`。



//Head只是一个哨兵节点
public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode()



//线程安全的双向链表
public actual open class LockFreeLinkedListNode {
private val _next = atomic(this) // Node | Removed | OpDescriptor
private val _prev = atomic(this) // Node to the left (cannot be marked as removed)
private val _removedRef = atomic<Removed?>(null)


关于这个数据结构,这里不做过多分析,等后面有时间可以专门研究一下,这个线程安全的数据结构,有如下特点:


* 它是一个双向链表结构,按理说双向链表的插入可以从头或者尾都是可以的,但是在这里,定义了插入只能是尾部,即右边;而获取元素,只能从头部,即左边。
* 它有一个哨兵节点,哨兵节点是不存储数据的,它的`next`节点是数据节点的头节点,它的pre节点是数据节点的尾节点,当数据节点为空时,依旧有哨兵节点。
* 该数据结构中,保存数据使用了`atomic`,即`CAS`技术,这样可以保证这个链表的操作是线程安全的。


到这里,我们已经知道了在`AbstractChannel`中存在一个线程安全的双向队列,至于节点保存的数据是什么,后面待会再分析。


### `send`流程分析


我们以文章开始的测试代码为例,当调用send(0)时,实现方法就是AbstractChannel中:



//发送数据
public final override suspend fun send(element: E) {
// fast path – try offer non-blocking
if (offerInternal(element) === OFFER_SUCCESS) return
// slow-path does suspend or throws exception
//挂起函数
return sendSuspend(element)
}


在该方法中,有2个分支,当`offerInternal`方法返回结果为`OFFER_SUCCESS`时,就直接`return`,否则调用挂起发送函数`sendSuspend`。  
 看到这个`offerInternal(element)`方法,我相信肯定会立马和前面所说的队列结合起来,因为offer这个单词就属于队列中的一种术语,表示增加的意思,和`add`一样,但是返回值不一样。  
 所以我们可以大致猜出该方法作用:把`element`添加到队列中,如果添加成功,则直接返回,否则则挂起。我们来看看`offerInternal()`方法:



//尝试往buffer中增加元素,或者给消费者增加元素
protected open fun offerInternal(element: E): Any {
while (true) {
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
}
}
}


该方法会往`buffer`中或者消费者增加数据,会成功返回数据,或者增加失败。  
 根据前面我们设置的是默认`Channel`,是没有`buffer`的,且没有调用`receive`,即也没有消费者,所以这里会直接返回`OFFER_FAILED`。  
 所以我们执行流程跳转到`sendSuspend`:



//send的挂起函数
private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont ->
loop@ while (true) {
//buffer是否已满,本例中,是满的
if (isFullImpl) {
//封装为SendElement
val send = if (onUndeliveredElement == null)
SendElement(element, cont) else
SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement)
//入队
val enqueueResult = enqueueSend(send)
when {
enqueueResult == null -> { // enqueued successfully
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”)
}
}

}
}


这就是send的挂起函数方式实现,分析:


* 这里使用`suspendCancellableCoroutineReusable`挂起函数,和我们之前所说的`suspendCancellableCoroutine{}`高阶函数一样,属于能接触到的最底层实现挂起函数的方法了,其中cont就是用来向挂起函数外部传递数据。
* 在实现体中,首先判断`isFullImpl`即是否满了,由于本例测试代码的`Channel`是没有容量的,所以是满的。
* 然后把`element`和`cont`封装为`SendElement`对象,这里的element就是我们之前所发送的0, 而`continuation`则代表后续的操作。  
 这个`SendElement`类定义如下:



//发送元素
internal open class SendElement(
override val pollResult: E,
@JvmField val cont: CancellableContinuation
) : Send() {
override fun tryResumeSend(otherOp: PrepareOp?): Symbol? {
val token = cont.tryResume(Unit, otherOp?.desc) ?: return null
assert { token === RESUME_TOKEN } // the only other possible result
// We can call finishPrepare only after successful tryResume, so that only good affected node is saved
otherOp?.finishPrepare() // finish preparations
return RESUME_TOKEN
}

override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN)
override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException)
override fun toString(): String = “ c l a s s S i m p l e N a m e @ classSimpleName@ classSimpleName@hexAddress($pollResult)”
}


从这里我们可以看出,这个`Element`就是把要发送的元素和`Continuation`给包装起来,而前面所说的双向链表中的元素也就是这种`Element`。


* 接着调用`enqueueSend`方法,把上面这个`Element`入队,根据该方法的返回值定义,这里会返回`null`,表示插入成功。
* 然后当入队成功时,会调用下面代码块:



enqueueResult == null -> { // enqueued successfully
cont.removeOnCancellation(send)
return@sc
}


这里先是给`cont`设置了一个监听:



//给CancellableContinuation设置监听
internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) =
invokeOnCancellation(handler = RemoveOnCancel(node).asHandler)



//当Continuation被取消时,节点自动从队列中remove掉
private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : BeforeResumeCancelHandler() {
override fun invoke(cause: Throwable?) { node.remove() }
override fun toString() = “RemoveOnCancel[$node]”
}


这个监听作用就是当`Continuation`执行完成或者被取消时,该节点可以从双向队列中被移除。  
 然后就是`return@sc`,这里是不是很疑惑呢?在以前我们实现挂起函数时,都是通过`continuation`的`resume`方法来传递挂起函数的值,同时也是恢复的步骤,这里居然没有恢复。那这个挂起函数该什么时候恢复呢?Channel是如何来恢复的呢?


### receive流程分析


我们接着分析,其实就是当调用`receive()`的时候。  
 `receive()`的实现,根据前面分析就是在`AbstractChannel`中:



//接收方法的实现
public final override suspend fun receive(): E {
// fast path – try poll non-blocking
val result = pollInternal()
@Suppress(“UNCHECKED_CAST”)
if (result !== POLL_FAILED && result !is Closed<*>) return result as E
// slow-path does suspend
return receiveSuspend(RECEIVE_THROWS_ON_CLOSE)
}


这里同样是类似的逻辑,首先是`pollInternal`方法,这里的`poll`同样和`offer`一样,属于队列的术语,有轮询的意思,和`remove`类似的意思,所以该方法就是从队列中取出元素,我们来看看实现:



//尝试从buffer或者发送端中取出元素
protected open fun pollInternal(): Any? {
while (true) {
//取出SendElement
val send = takeFirstSendOrPeekClosed() ?: return POLL_FAILED
//注释1
val token = send.tryResumeSend(null)
if (token != null) {
assert { token === RESUME_TOKEN }
//注释2
send.completeResumeSend()
return send.pollResult
}
// too late, already cancelled, but we removed it from the queue and need to notify on undelivered element
send.undeliveredElement()
}
}


根据前面我们`send`的流程,这时可以成功取出我们之前入队的`SendElement`对象,然后调用注释2处的`send.completeResumeSend()`方法:



override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN)


这里会调用`continuation`的`completeResume`方法,这里就需要结合前面文章所说的原理了,其实这个continuation就是状态机,它会回调`CancellableContinuationImpl`中的`completeResume`:



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


而该类的继承关系:



internal open class CancellableContinuationImpl(
final override val delegate: Continuation,
resumeMode: Int
) : DispatchedTask(resumeMode), CancellableContinuation, CoroutineStackFrame


这里相关的类,我们在线程调度那篇文章中有所提及,这里的`dispatchResume`:


![img](https://img-blog.csdnimg.cn/img_convert/ebf282c5ece7f79f7a785a16f000e95c.png)
![img](https://img-blog.csdnimg.cn/img_convert/72586d7353350617778a765dfd078759.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618636735)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

lableContinuation<T>, CoroutineStackFrame 

这里相关的类,我们在线程调度那篇文章中有所提及,这里的dispatchResume:

[外链图片转存中…(img-v9NU4aQO-1715635369315)]
[外链图片转存中…(img-FM10PMO6-1715635369315)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值