kotlin协程的简述和使用

为什么会有协程 && 什么是协程

当我们最初学习程序之时,我们书写代码、使用指令执行,完成逻辑链条的前后关系。代码执行到哪,逻辑就走到哪。但问题随之出现,有些过程并不是能立即得到结果的,监听按钮或者一些耗时操作如IO操作等,程序为了等待结果就会阻塞。在一些应用场景之下,我们常常会使用异步api,通过一些回调函数操作来完成异步任务。

kotlin协程的简述和使用_第1张图片

但是异步回调也有本身的问题,第一是,原本的统一的同步逻辑被拆分成几个阶段,造成代码可读性不好。第二是,在复杂的应用场景下,可能造成十分难受的回调地狱。以及难以debug的问题。

// JavaScript展示地狱回调
setTimeout(function () {  // 第一层
    console.log('第一层'); // 等3秒打印,再执行下一个回调函数
    setTimeout(function () {  // 第二层
        console.log('第二层'); // 等2秒打印,再执行下一个回调函数
        setTimeout(function () {   // 第三层
            console.log('第三层'); // 等1秒打印
        }, 1000)
    }, 2000)
}, 3000)

协程则应运而生。协程就是协作式多任务,协程本身是一种任务调度机制,也可以说是一种编程设计思想并不限定语言(在1958年就被发明,并用于构建汇编程序)。协程可以使用同步的代码逻辑流去操作异步的控制流。并且不会导致在操作系统的线程阻塞。相对于线程之间的切换,协程是更轻量级的。对于线程的操作是调用了操作系统的功能,而启用协程则是编程语言来完成,所以协程也被称为用户态线程。

协程为并发而生,线程在CPU多核之下,已然能完成并行。如图所示,并发本身是对CPU时间片的争夺。而对于异步任务进行线程切换,这才是协程能做的事。下图为CPU时间片段对于串行、并行、并发的线程处理的区别。

kotlin协程的简述和使用_第2张图片

协程对于异步任务是主动让出而不是抢占的多任务,突出是主动让出;而线程则是抢占式的多任务,突出被动抢占。

抢占是一个相对低效的操作,“打断”这种操作不是那么容易做的,操作系统级别上之所以需要抢占是为了避免任务占着CPU不走。但在你自己知根知底的代码还要去抢占,这基本上就是牺牲性能的操作。所以使用协程避免抢占可以提高性能。

部分语言对于协程的支持

  • go语言的协程,直接在语法层面支持协程。解决了服务端开发中,IO密集型任务,并发性能程序过于复杂的痛点。go可以很优雅地进行高并发场景的开发。go语言的协程叫 Goroutines,从英文拼写就知道它和 Coroutines 还是有些差别的(设计思想上是有关系的),不然Kotlin的协程完全可以叫 Koroutines。
  • 之前Java对于协程而言,可以使用NIO(new IO)和一些多线程api进行操作,能模拟出一定的协程的效果,但实际开发方面还是过于麻烦,也许go语言在服务器端异军突起,在协程并发方面Java落于下风不无关系。
  • 2022年9月份,Oracle正式发布了最新版本的Java19和对应的Java虚拟线程的特性。是为帮助提高大型服务器的应用性能。Java虚拟线程在设计思想上,就是轻量级线程。值得关注的是,这次改动对于Java各个Api的改动很少,并没有太多新语法。Java19的虚拟线程是预览特性,很可能在Java21才会成为正式特性。
  • 很多语言都有自己的协程(虚拟线程)的技术,除了Go、Java,还有C#、Erlang、Lua等等。

kotlin协程

协程概念本身与线程概念是同一级别的东西。

在kotlin-JVM、Android平台中,对于协程的运用本质成为了切换线程,在性能上本身不会比优化后的线程池强。这是kotlin-JVM语言对于协程的实现。甚至在其他平台,kotlin-native、kotlin-javascript上,协程的实现的方法都完全不一样。在这个基础上,可以说,协程未必等于切线程,而未必就不能强于线程池。只是kotlin-JVM的协程实现就是基于切换线程。

kotlin协程写法

kotlin协程的写法和运用方案很多,有:runBlocking顶层函数;GlobalScope(CoroutineScope)单例对象调用launch开启协程;创建一个 CoroutineScope 对象开启协程等等。

这里我主要讲比较简单的开启协程的方法。

CoroutineScope(Dispatchers.Main).launch {
    // your code
    
    withContext(Dispatchers.IO) {
        // your blocking code
    }

    // your code

    withContext(Dispatchers.IO) {
        // your blocking code
    }
    
    // your code
    
}

如上的写法就是在main线程中进行异步操作,将阻塞和耗时操作放入withContext的切换线程的代码块里,kotlin程序就自然帮我们完成了线程之间的切换,以同步的写法完成异步的事情,这就是协程。

如下的代码是实现一个计时器更新主线程button的文字的功能,是kotlin使用协程对比使用线程池,kotlin-JVM协程在写法和性能上不一定更优秀,这些都是设计思想和方案选择的碰撞和抉择。如下代码,进行了简单的时间增加并在Android的UI线程更新的逻辑。

private fun useCoroutines() {
    CoroutineScope(Dispatchers.Main).launch {
        buttonCount.isEnabled = false
        var count = 0
        while (count < 10) {
            withContext(Dispatchers.IO) {
                delay(SECOND_DURATION)
                count++
            }
            buttonCount.text = String.format(
                Locale.getDefault(),
                "%d",
                count
            )
        }
        buttonCount.isEnabled = true
    }
}
private fun useExecutor() {
    Executors.newSingleThreadExecutor().execute {
        var count = 0
        while (count < 10) {
            SystemClock.sleep(SECOND_DURATION)
            count++
            val finalCount = count
            runOnUiThread {
                buttonCount.text = String.format(
                    Locale.getDefault(),
                    "%d",
                    finalCount
                )
            }
        }
        runOnUiThread { buttonCount.isEnabled = true }
    }
}

suspend关键字

suspend 是 Kotlin 协程最核心的关键字之一。官方解释是,代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。

什么是挂起函数

suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或者其他挂起函数内使用。协程内部挂起函数的调用处被称为挂起点,也就是Android Studio代码左边会出现的一个箭头加一个绿色波浪线的标志。

网上寻到的gif例子,具体挂起函数演示如下:

getUserInfo()、getFriendList(user)、getFeedList(friendList)三个函数都是挂起函数。内部必然为 suspend修饰的挂起函数。例如getUserInfo函数可为下内容:

suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) { // 切换到IO线程
        delay(1000L) // 延迟1s
    }
    return "content"
}

挂起的对象是协程,挂起的本质“就是这个协程从正在执行它的线程上脱离”。注意,这里不是这个协程停下来,而是脱离,从当前线程脱离,比如上文的代码,就是从主线程脱离,去IO或工作线程上进行处理。紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。如上文所示,我们代码本身在主线程允许,当协程切走的函数执行完毕,协程会帮助我们post一个Runnable,让我们剩下的代码和信息继续回到之前的线程去运行。

参考文献

  • 市面上的协程有什么本质上的区别
  • 出于什么样的原因,诞生了「协程」这一概念?
  • 线程和协程的区别的通俗说明
  • 协程到底是什么
  • Kotlin 的协程
  • 到底什么是「非阻塞式」挂起?协程真的比线程更轻量级吗?

你可能感兴趣的:(kotlin,android,开发语言)