面试官:说几个同步拿到异步操作结果的方式

0 阅读5分钟

最近工作中遇到个问题,感觉可以拿出来讲一下,因为这个问题的处理方式有很多种,涵盖的知识点也不少,我想如果我来作为一个面试官来问人的话,这个问题我是会拿出来作为考题的,先来看下一段代码

image.png

这段代码的意思很简单,在一个函数内会创建一个弹框,点击弹框上的按钮会改变变量name的值,随后将变量返回出去,但是众所周知弹框的点击事件是一个异步操作,而函数的return语句是会在弹框创建后同步执行的,所以这段代码无论弹框内怎么改变name的值,最终函数的执行结果永远都是一样的,也就是熊大,那么有没有办法将这段代码更改一下,让最终执行结果可以跟最初设计预期一样,只有当点击弹框按钮改变name值并且关闭弹框后,才将name值返回出去,也就是同步处理弹框的异步操作

CountDownLatch

说到同步,我们会想到许多方案,那么CountDownLatch肯定会是这些诸多方案中的其中一种,它允许线程去等待其他线程执行完成后再继续执行,执行的时机主要是靠里面的计数器,只有当计数器倒计时到0后,线程才可以继续执行

image.png

我们在代码中将CountDownLatch添加进去就变成了上面这样,return语句的上面多了一行latch.await(),这个时候计数器如果不倒计时到0,return语句是不会执行的,只有当点击了弹框上的按钮之后,计数器倒计时-1,这样才会执行return语句,这个时候,name就已经变成熊二了,但是事实真的如此吗?我们加个测试按钮执行一遍上述代码试试

image.png 001-resize.gif

代码执行后发现不但没有弹出框子,应用甚至还anr了,造成这个问题的原因主要还是因为CountDownLatch有阻塞线程的作用,在这里我们的线程自然就是主线程,所以肯定会阻塞的,但这是不是说明CountDownLatch不适合用在此类问题上呢?当然不是,我们将代码改一下

image.png

仅仅只是把需要阻塞的线程改成了非UI主线程,CountDownLatch的作用就能发挥出来了

002-ezgif.gif

所以我们得到的结论是CountDownLatch是可以处理一些异步操作同步处理的问题,但是要分场景,如果CountDownLatch需要阻塞的线程是主线程的话,就不推荐使用这个方案了

suspendCoroutine

言归正传,我们的框子还没出来呢,问题还没解决,既然CountDownLatch无法满足我们这个需求,我们需要想一想其他方案,在上面的例子中提到了几个关键点,阻塞,恢复,执行...脑海里面是不是立马就蹦出来协程俩字,阻塞也就相当于挂起,区别就是阻塞是阻的线程,而挂起挂的是协程,我们只需要将弹框放在一个挂起函数里面,然后点击完弹框按钮后将其恢复就可以了,这里我们使用suspendCoroutine函数来实现

image.png

使用suspendCoroutine的原因是我们需要用到它lambda表达式中的参数Continuation,因为这里的协程从挂起到恢复的时机就是点击弹框按钮,所以需要在点击按钮的时候手动触发Continuation.resume来恢复协程,否则协程将会一直处于挂起状态,resume里面的参数就是需要从协程内部返回给外界的值

003-ezgif.gif

这样就完成了使用同步的方式处理异步操作结果的效果了,当然除了suspendCoroutine之外,还有别的方式

Channel

协程中同步处理异步操作的方式有很多,刚才说的suspendCoroutine是基于协程的挂起恢复来实现,接下来说的Channel除了一部分挂起恢复的思想外,也基于生产者消费者的思想来实现

image.png

Channel相当于是一个消息管道,当调用了receive函数的时候,如果管道里面没有数据,协程会处于挂起状态,只有当生产者往里面添加数据的时候,receive函数才会恢复协程,并拿到生产者丢进来的数据,那么我们这里的生产者就是弹框,点击按钮就是生产数据,完整的代码如下

image.png

这段代码,消费者receive之后一直在等待生产者发送消息,这个时候协程是挂起的,随后在弹框中点击了按钮,调用了send函数之后,才在消息管道中添加了一条新的数据,receive函数收到数据后,协程恢复,数据被外界获得

CompletableDeferred

CompletableDeferred的用法与Channel很相似,但是它们也存在着一些差异

  • 通信方式CompletableDeferred是单向单次通信,Channel是双向持续通信
  • 数据量CompletableDeferred只能传递一个值,Channel由于是管道,所以可以传递多个值
  • 生命周期:CompletableDeferred完成后立即失效,Channel可长期保持开放状态

上述例子使用CompletableDeferred实现的代码如下,代码很相似

image.png

总结

其实如果撇开同步这个因素,其实文中的例子用LiveData或者Flow也是可以实现同样的目的,关键还是遇到的场景以及问题必须得用同步的方式解决,文章写完了,希望对大家有帮助