Kotlin | 从线程到协程,你是否还存在 [同步] 上的使用疑问

2022-03-01 11:56:51 浏览数 (1)

Kotlin | 从线程到协程,你是否还存在理解上的疑问

引言

在2022的今天,对于一个 Android 开发同学,如果你使用 Kotlin 作为主要开发语言,那么协程是必不可缺的 异步框架 。不过对于初学者来说,有时候依然存在一些理解问题或者使用上的不解。毕竟我们用了那么多年的回调与线程,突然转变思想,的确需要过程。

本文将结合实际中其他同学遇到的问题来讲讲,从线程到协程,初学者对于 `[同步]` 的理解疑问。

背景

事情源自这样,今天早上在群里,发现有同学问到了这样一个问题:

协程A:开启一个等待页面,wait,等到B完成后显示成功 协程B:与下位机通讯,等到下位机回复成功后,通知A协程 notify

具体对话图示如下:

这个同学的想法是:

开启两个协程,协程A开启一个等待页面,然后在这里 wait 等待;等协程B这边执行成功后,再通知协程A去刷新。

作为过来人,我们不难第一反应,协程默认不就是同步吗,直接 suspend 就完了啊?为什么要通知呢?不是很麻烦吗?

解决这个问题很简单,但我的第一反应是,他似乎理解错了协程中的同步?但反过来又仔细一想,这个同学为什么能存在疑问,似乎我也曾问过,为什么不可以等待另一个job来通知我完成了呢?所以我更想告诉他为什么要这样写?

对于初使用协程而言,我们的想法应该怎样转变,这也即本文的主章:

面对协程,我们应该怎样去接受解决思路的转变

解决方法

在阐述 [莫须有] 的思想之前,我先写出下面的不同解法,以便大家更好的体会差异:

1. 线程写法

定义两个线程,线程A开始,然后 wait 等待,线程B执行逻辑,成功后,调用线程A notify.

代码语言:javascript复制
fun threadTest() {
    // job A
    val jobA = thread(start = false) {
        println("开始执行成功逻辑")
    }
    jobA.wait()
    // job B
    thread {
        Thread.sleep(1000)
        jobA.notify()
    }
}

2. 接口回调

如果用 回调 去做,免除 阻塞线程 ,又是这样的写法:

定义一个接口,任务A开始执行,在这里等,等另一边任务B完成后,再调用任务A接口方法即可完成唤醒。

代码语言:javascript复制
fun callBackTest() {
    val jobA = {
        println("开始执行成功逻辑")
    }
    // jobB开始
    thread {
        Thread.sleep(1000)
        jobA.invoke()
    }
}

3. 协程

解析

Android 官网中,对协程的描述如下:

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化 异步执行 的代码。

说简单点就是,在协程的世界中,一切都是同步,按顺序进行。即一步接一步,我们等待上一步的结果,然后决定是否继续执行下一步。

综合对比上述的解法来看:

  1. 线程写法:我们需要调用 await ,这将使得正在运行的线程[阻塞],对我们的性能造成了影响;
  2. 回调写法:我们不再阻塞线程,但我们逻辑更复杂化,如果存在多个回调,这将提高阅读成本;
  3. 协程写法:我们提供了两种不同的写法,即是否需要改善相应方法中的回调。
    • 前者在执行任务B时,我们切换到了 IO协程 ,并最终将状态返回,接下来,我们判断,如果获得的state是我们想要的写法,就继续操作;
    • 后者在执行任务B时,利用了suspendCoroutine 函数,我们可以将一些回调的代码借此改为协程的同步写法,从而获得与前者一致的体验;

所以协程具有如下的基本特点:

更轻量简化异步代码

而面对难解决的异步代码时,我们首要的不应该考虑如何去通知,而是看看能不能将任务拆分,比如将原有需要通知的这一步拆为三步走:

在非协程的世界,我们可能想,先执行任务A,等待任务B成功后,再去通知A继续执行。

而在协程的世界,我们就可以改为:先执行任务A前奏,再去执行任务B,根据任务B的结果决定是否继续执行任务A的后步骤。

扩展

下面这些函数,对于初学者可能会比较有帮助。

suspend

将一段普通代码转换为挂起函数

代码语言:javascript复制
suspend {
    delay(1000)
    withContext(Dispatchers.IO){
        
    }
}

将回调代码转为协程

suspendCoroutine
代码语言:javascript复制
 // 可取消的挂起函数
 suspendCoroutine<Int> {
      // 便于将一些回调操作改善为同步写法
      // 成功
      it.resumeWith(Result.success(1))
      // 异常 or 失败
      it.resumeWith(Result.failure(RuntimeException("异常")))
​
      // 对于上述的ktx扩展
      // 扩展函数,对于resumeWith-success的封装
      it.resume(1)
      // 扩展函数,对于resumeWith-failure的封装
      it.resumeWithException(RuntimeException("异常"))
}
suspendCancellableCoroutine

对比前者多了 cancel ,即在内部取消此协程,取消时会抛出异常 CancellationException

后记

本文是比较简单入门的一篇文章,也是回复其他同学后,做的一个记录。虽然对我们而言,看着的确很简单,但在开始的路上,有问题并提出来总是好的。

0 人点赞