Kotlin 协程(一) ——— 简介

1,050 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、进程、线程与协程

在计算机中,通常我们把一个任务称为一个进程。例如:浏览器是一个进程,音乐播放器是一个进程,Word 是一个进程。在 Android 开发中,通常一个应用就是一个进程。

一个进程中可以有多个线程,并且至少有一个线程,比如我们可以在应用中一边下载文件,一边浏览网页,这时候就是两个或多个线程在一个进程中同时运行。

操作系统调度的最小任务单位就是线程。无论是 Linux 还是 Windows,都采用了抢占式多任务,如何调度线程完全由系统决定,程序无法控制线程什么时候调度。

——— 那么,协程又是什么呢?

协程与线程的关系类似线程和进程的关系:一个线程中可以有多个协程。协程和线程的不同点在于,协程的调度完全由开发者控制,这就给了开发人员很大的灵活性。并且协程也比线程更轻量,通常使用协程来实现多任务能获得比多线程更好的性能。

注:由于 Kotlin 语言是基于 JVM 的,而 JVM 底层并没有对协程的支持,所以 Kotlin 中的协程本质上还是靠线程池实现的。但协程和线程并不是同一级别的概念,在其他语言中的协程可能与线程完全不一样,Kotlin 以线程池实现协程实属无奈之举。但在开发层面上,Kotlin 中的协程使用起来和其他语言的协程是类似的。

二、协程的优势

  • 便捷:协程使得一个函数可以自动挂起和恢复,在进行耗时任务时,不必使用接口回调的方式等待耗时任务的结果。这种陈述式的编程,符合人类串行思维,避免了回调地狱。

  • 高效:前文已提到过,协程比线程更轻量,性能更好。

  • 灵活:由于协程的调度由开发者控制,使用起来更为灵活。

三、协程的基础设施与上层框架

Kotlin 的协程分为基础设施与上层框架。基础设施层的内容存放在 kotlin 包中,上层框架层的内容存放在 kotlinx 包中。

比如:

import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.createCoroutine
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

3.1. 用协程的基础设施实现协程

使用 Kotlin 协程的基础设施实现协程较为繁琐,使用基础设施实现协程的代码类似这样:

// 创建协程
val continuation = suspend {
    1
}.createCoroutine(object : Continuation<Int> {
    override val context: CoroutineContext
        get() = EmptyCoroutineContext
    override fun resumeWith(result: Result<Int>) {
        Log.d("~~~", "result: $result")
    }
})
// 启动协程
continuation.resume(Unit)

运行程序,输出如下:

~~~: result: Success(1)

其中的 CoroutineContext 就会用来记录挂起点。

通常我们开发时不会使用太多基础设施层的东西,主要使用的是 Kotlin 协程的上层框架,也就是 kotlinx 包中的东西。

3.2. 用上层框架开启一个协程

使用 Kotlin 协程上层框架开启协程的最简单方式就是使用 GlobalScope.launch,GlobalScope 表示全局作用域,用来开启一个顶级协程。

fun main() {
    GlobalScope.launch {
        println("coroutine scope")
    }
    // 等待一秒钟保证协程运行完毕。
    Thread.sleep(1000)
}

关于协程作用域、协程上下文、launch 方法等内容会在后续文章中逐一讲解。

四、挂起函数

在协程中执行耗时任务时,通常我们会将耗时任务放到挂起函数中,挂起函数由 suspend 关键字修饰:

fun main() {
    GlobalScope.launch {
        delaySomeTime()
        println("coroutine scope")
    }
    // 等待一秒钟保证协程运行完毕。
    Thread.sleep(1000)
}

private suspend fun delaySomeTime() {
    delay(1000)
}

在本例中,delay 函数就是 kotlinx 中自带的一个挂起函数,它的作用是使协程挂起一段时间。挂起时不会阻塞当前线程,当前线程仍然可以继续执行其他任务,当挂起时间到后,挂起函数又会从挂起点恢复执行。

一般函数只包含 invoke 和 return 操作,代表执行和返回。挂起函数不仅包含 invoke 和 return,还包含 suspend 和 resume 操作,这两个操作代表挂起和恢复。

注:suspend 关键字只是标志这个函数中可能有挂起操作,本身不提供挂起的功能。

五、开启协程:launch VS async

launch 和 async 都可以用来开启一个协程。两者的区别是:

  • launch 启动的协程不带有返回值。
  • async 启动的协程以最后一行作为返回值,通过 await 可以获得其返回值。

六、阻塞协程:join VS await

如果需要等待 launch 开启的协程执行完毕,可以使用 join() 方法。join() 会阻塞当前线程,等待协程执行完毕后才释放。

如果需要等待 async 开启的协程执行完毕,可以使用 await() 方法,同样地,await() 也会阻塞当前线程。

如果想要多个协程并发执行,并且获得多个返回值,可以先调用 async,最后统一调用 await(),这样能让协程并发执行。不要在每个协程 async 开启后,马上调用 await(),因为 await() 会阻塞当前线程。这样开启的多个协程会串行执行,效率较低。

举个例子:

如果 async 开启协程后,马上调用 await(),这种写法会让两个协程串行执行,效率低:

runBlocking {
    val start = System.currentTimeMillis()
    val result1 = async {
        delay(1000)
        1 + 1
    }.await()
    val result2 = async {
        delay(1000)
        1 + 1
    }.await()
    println("result = ${result1 + result2}")
    // await 会阻塞协程,所以这种写法会导致两个协程串行执行,耗时 2 秒左右
    println(System.currentTimeMillis() - start)
}

改成这样,先用 async 开启两个协程,再统一调用 await(),让两个协程并行执行,效率更高:

runBlocking {
    val start = System.currentTimeMillis()
    val deferred1 = async {
        delay(1000)
        1 + 1
    }
    val deferred2 = async {
        delay(1000)
        1 + 1
    }
    val result1 = deferred1.await()
    val result2 = deferred2.await()
    println("result = ${result1 + result2}")
    // 统一调用 await 方法,让两个协程并行执行,耗时 1 秒左右,效率高
    println(System.currentTimeMillis() - start)
}

这两段代码执行的是一样的逻辑,但第二种写法只需要 1s 左右就能执行完成,第一种写法却需要 2s 左右,所以使用协程时需要开发者控制好程序的并发,以提高程序运行效率。

七、小结

本文简要介绍了协程、挂起函数,以及协程的一些基本用法。后续笔者会持续更新协程相关的教程,争取拿到「新人创作礼」,请各位道友监督(=,=)