---
title: 如何使用Kotlin协程中的suspend函数
date: 2021-04-02 16:37:14
tags: [JVM, Kotlin, Coroutine, suspend, 协程, 并发]
categories:
  - [JVM, Kotlin]
---
协程是Kotlin带来的一项明星功能。通过使用比线程更加轻量的协程，程序的性能得到了极大的提高。但是协程的运行控制有与传统的线程不尽相同，尤其是suspend函数的引入，更加使协程的使用令人迷惑。本文试图通过使用更加简单的方式对如何使用Kotlin协程进行简述。
<!--more-->

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

## 协程的控制

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

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

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

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

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

## 协程的启动

在Kotlin程序中启动一个协程，可以使用`launch`、`async`、`runBlocking`。

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

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

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

- `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函数在书写起来就跟普通函数没有什么两样，例如：

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

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

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

## 合成示例

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

```kotlin
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) {
            // 执行需要消耗较多算力的任务
        } }
}
```
