blog/source/_posts/kotlin-coroutines-suspend-function.md
2021-05-13 10:19:30 +08:00

6.1 KiB
Raw Blame History

title date tags categories keywords
如何使用Kotlin协程中的suspend函数 2021-04-02 16:37:14
JVM
Kotlin
Coroutine
suspend
协程
并发
JVM
Kotlin
JVM,Kotlin,Coroutine,suspend,并发,协程

协程是Kotlin带来的一项明星功能。通过使用比线程更加轻量的协程程序的性能得到了极大的提高。但是协程的运行控制有与传统的线程不尽相同尤其是suspend函数的引入更加使协程的使用令人迷惑。本文试图通过使用更加简单的方式对如何使用Kotlin协程进行简述。

需要注意的是虽然Kotlin Coroutines声称其是使用的协程但是在功能的底层实现上依旧还是多线程只是多线程的调度和交互已经被Kotlin Coroutines做了极大的优化。

协程的控制

suspend函数是发挥协程的性能威力妥善处理异步的核心。不同于Go语言中的协程Kotlin所提供的协程实际上是一套“线程框架”其对任务的处理方式更加类似于Java中的Executor。所以对于协程的控制实际上是可以借用线程的控制的。

在整个操作系统的概念中是没有协程这个概念的我们所有程序的代码都是在线程中运行的也就是说如果没有使用多线程API专门启动其他的线程我们的程序是以单线程的方式运行的。而线程又是依附于进程的在一个进程中可以存在众多的线程。协程的概念首先是Go语言提出来的Go中的协程是一个比线程更加轻量的结构一个线程中可以轻松的运行若干个协程而且这个协程的结构是根植与Go语言的核心中的。

但是Kotlin首先是一种JVM语言不像Go语言那样没有历史包袱。Kotlin所提供的协程是一种可以在一个线程中运行并完成调度然后将一些比较耗时或者需要等待的放到其他的线程中去运行这样就可以让运行核心程序的主线程能够不被“阻塞”。所以协程的特点就是可以使用同步代码的形式写出异步的程序。

不同于Java中使用回调函数来处理异步协程利用“挂起”来处理异步。suspend函数在整个协程中标记了所有耗时和需要在其他线程中处理的任务每当协程遇到suspend函数的时候就会把函数的执行放到其他的线程中去执行然后将当前的协程挂起这样程序的主线程就可以去执行其他的协程了。所以Kotlin协程的调度方法可以参考以下示意图。

所以一个协程的“挂起”实际上就是让这个协程从当前的线程上脱离去了调度器给它指定的线程去并行运行了。但是等到这个协程运行结束以后Kotlin协程框架还会自动的把它切回来这个操作就像是协程在之前的线程上被唤醒了一样。

协程的启动

在Kotlin程序中启动一个协程可以使用launchasyncrunBlocking

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主线程中的代码会立即执行
    runBlocking {     // 但是这个表达式阻塞了主线程
        delay(2000L)  // 我们延迟 2 秒来保证 JVM 的存活
    } 
}

使用launchasync的时候,需要指定协程的作用域,例如上例中的GlobalScope即表示程序的全局作用域,在这个作用域中是可以启动后台协程的。一个比较常见的是直接调用launchasync,这样的话新协程将会继承其父级协程的上下文,变成一个子协程。

启动协程的这三个函数的区别如下:

  • launch,启动一个协程,但并不关心其内部的返回结果。
  • async,启动一个协程,可以从其中返回结果。协程的运行结果可以使用.await()获取。
  • runBlocking,启动一个协程,但是这个协程将会阻塞启动它的线程。通常会在单元测试中用到。

Dispatchers

启动协程的函数launch、async还可以接受Dispatcher参数用于指示即将启动的协程要如何调度常用的调度有以下几种并且会随着项目引入的其他依赖出现新的调度器。

  • Dispatchers.Main,程序的主线程。
  • Dispatchers.IO针对磁盘和网络优化的IO线程。
  • Dispatchers.Default适用于CPU密集型任务的线程。
  • Dispatchers.SwingSwing进行UI渲染的线程。
  • Dispatchers.JavaFxJavaFx进行UI渲染的线程。

suspend函数

其实描述到这里suspend函数已经没有什么秘密了suspend关键字实际上不执行任何挂起协程的功能它存在的唯一意义就是提醒开发者这个函数是一个异步函数需要被放到协程中去执行。所以一个suspend函数在书写起来就跟普通函数没有什么两样例如

suspend fun suspendUntilDone() {
    while (!done) {
        delay(5)
    }
}

但是一般suspend函数最好还是使用withContext指示一下这个suspend函数需要使用哪种调度器例如下面这个示例

suspend fun callAPI(id: String) = withContext(Dispatchers.IO) {
    ...
}

合成示例

综合以上示例可以组成一个附带有后台协程和IO协程以及密集计算协程的示例。

suspend fun callApi(id: String): String = withContext(Dispatchers.IO) {
    // 执行网络访问
    return response;
}

// runBlocking会将main函数转换为一个协程否则就需要手工使用coroutineScope自行创建
fun main(arg: Array<String>) = runBlocking {
    GlobalScope.launch {
        // 创建在GlobalScope上的协程不会继承父级作用域
        while (true) {
            // 执行一些需要在后台提供服务的功能
            ...
        }
    }
    // 此处async启动的协程其作用域与runBlocking相同属于runBlocking的子协程
    val response = async { callApi("1") }
    response.await()
        .data.forEach { d -> launch(Dispatchers.Default) {
            // 执行需要消耗较多算力的任务
        } }
}