协程(22) Channel原理解析_xvpp002,2024年最新2024历年字节跳动HarmonyOS鸿蒙面试真题解析

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数HarmonyOS鸿蒙开发工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年HarmonyOS鸿蒙开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img

img
img
htt

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上HarmonyOS鸿蒙开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注鸿蒙获取)
img

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

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`:



private fun dispatchResume(mode: Int) {
if (tryResume()) return // completed before getResult invocation – bail out
// otherwise, getResult has already commenced, i.e. completed later or in other thread
dispatch(mode)
}



internal fun DispatchedTask.dispatch(mode: Int) {

if (dispatcher.isDispatchNeeded(context)) {
dispatcher.dispatch(context, this)
}

}


这里最终会调用`dispatcher.dispatch()`方法,而这个我们在之前调度器文章说过,这个最后会在Java线程池上执行,从而开始状态机。  
 既然该状态机恢复了,也就是前面`send`流程中的挂起也恢复了。  
 当`send`挂起函数恢复后,再通过



return send.pollResult


就可以获取我们之前发送的值0了。


同样的,当`pollInternal`方法中,无法`poll`出`SendElement`,则会调用`receiveSuspend`挂起方法:



private suspend fun 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
}
}
}


和`send`类似,这里也会封装为`ReceiveElement`,同时入队到队列中,等待着send方法来恢复这个协程。


### "热"的探究


分析完默认的`Channel`的发送和接收,我们来探究一下为什么Channel是热的。  
 这里所说的热是因为Channel会在不管有没有接收者的情况下,都会执行发送端的操作,当策略为`Suspend`时,它会一直持续到管道容量满。


这里我们还是拿之前文章的例子:



fun main() = runBlocking {
//创建管道 val channel = produce(capacity = 10) {
(1 … 3).forEach {
send(it)
logX(“Send $it”)
}
}
logX(“end”) }


这里虽然没有调用`receive`方法,即没有消费者,send依旧会执行,也就是"热"的。


根据前面所说的`Channel()`顶层函数源码,这里容量为10,策略不变,最终会创建出`ArrayChannel`实例。  
 该类定义:



internal open class ArrayChannel(
/**
* Buffer capacity.
*/
private val capacity: Int,
private val onBufferOverflow: BufferOverflow,
onUndeliveredElement: OnUndeliveredElement?
) : AbstractChannel(onUndeliveredElement)


这里同样是`AbstractChannel`的子类,所以send方法还是依旧:



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


还是先尝试往队列中offer数据,当无法offer时,执行挂起;但是这里的`offerInternal`方法在`ArrayChannel`中被重写了:



//ArrayChannel中的方法
protected override fun offerInternal(element: E): Any {
//接收者
var receive: ReceiveOrClosed? = null
//当多个线程都同时调用该方法时,为了容量安全,这里进行加锁
lock.withLock {
//元素个数
val size = this.size.value
//发送已经关闭,直接返回
closedForSend?.let { return it }
// update size before checking queue (!!!)
//在入队之前,更新管道容量,当元素小于管道容量,返回null
//只有管道中的元素个数,大于管道容量时,该方法才会return
//根据策略,会返回挂起或者丢弃或者失败等
updateBufferSize(size)?.let { return it }

//容量没满时,把元素入队
enqueueElement(size, element)
//返回入队成功
return OFFER_SUCCESS
}

}


在这里我们可以发现,不管有没有接收者的情况下,当我们多次调用send方法,当队列没满时,在这里都会返回`OFFER_SUCCESS`,即发送端已经在工作了,所以也就是我们所说的热的效果。


## 总结


`Channel`作为线程安全的管道,可以在协程之间通信,同时可以实现交替执行的效果,通过本篇文章学习,我相信已经知道其原因了。小小总结一下:


* `Channel`接口在设计时就非常巧妙,充分利用了接口和抽象,把发送端和接收端能力分开,这个值得我们学习。
* `Channel`的线程安全原因是发送端维护了一个线程安全的双向队列:LockFreeLinkedList,我们把值和`continutaion`封装为`SendElement/ReceiveElement`保存其中,这样就保证了线程安全。
* `Channel`的发送和接收挂起函数的恢复时机,是通过队列中的`continuation`控制,在`CancellableContinuationImpl`进行直接恢复,而不是我们常见的调用`resumeWith`方法。



> 
> 作者:元浩875  
>  链接:https://juejin.cn/post/7172451416340955144
> 
> 
> 


## 最后


如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。


如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


![在这里插入图片描述](https://img-blog.csdnimg.cn/06e41b3932164f0db07014d54e6e5626.png)  
 相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。


#### 全套视频资料:


**一、面试合集**  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/4f54728150894ebcb33d7d4b57501cae.png)  
 **二、源码解析合集**

规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。


如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


![在这里插入图片描述](https://img-blog.csdnimg.cn/06e41b3932164f0db07014d54e6e5626.png)  
 相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。


#### 全套视频资料:


**一、面试合集**  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/4f54728150894ebcb33d7d4b57501cae.png)  
 **二、源码解析合集**

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值