如何使用Kotlin协程中的suspend函数

发布时间:2021-04-02 16:37
最后更新:2021-04-02 16:37
所属分类:
JVM Kotlin

协程是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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.Swing,Swing进行UI渲染的线程。
  • Dispatchers.JavaFx,JavaFx进行UI渲染的线程。

suspend函数

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

1
2
3
4
5
suspend fun suspendUntilDone() {
    while (!done) {
        delay(5)
    }
}

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

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

合成示例

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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) {
            // 执行需要消耗较多算力的任务
        } }
}

索引标签
JVM
Kotlin
Coroutine
suspend
协程
并发