Kotlin 协程看这一篇就够了

2,138 阅读7分钟

前言

Kotlin 是 Google 官方极力推荐的 Android 端开发语言,而 Kotlin 协程是一种并发设计模式,简化 Android 平台异步编程。所以掌握协程的使用是非常有必要的。

什么是协程?

  • 协程本质上就是轻量级的线程
  • 线程框架(API)

相对于 Java 线程的优势?

  • 轻量级:支持开启十万个协程处理异步任务(这十万个协程可能在同一个线程),如果使用线程来实现很可能会产生内存不足的错误,在64位 Linux 上 hotspot 的一个线程栈容量默认是 1MB,内核数据结构会额外消耗 16kb 内存,与之相对的一个协程栈通常在几百 Kb 到几 KB 之间(摘自 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第3版))
  • 编码风格的改变:同步的方式写异步的代码,避免了回调地狱
  • Jetpack 集成:JetPack 库基本都提供了自己的协程作用域,可用于结构化并发
  • 内存泄漏更少:结构化并发可以保证代码更加安全,避免了协程的泄漏问题

调度器

  • Dispatchers.Main:可在 Android 主线程上运行协程,用于界面交互
  • Dispatchers.IO:经过优化,适合执行 IO 密集型任务如:磁盘或网络 I/O 操作,默认限制线程数为 64,类似于 Java 的 IO 线程池
  • Dispatchers.Default:经过优化,适合执行 CPU 密集型任务如:列表排序和解析JSON,最大线程数和 CPU 核心数相关,类似于 Java 的 CPU 线程池
  • Dispatchers.Unconfined:在调用它的线程中启动一个协程,运行在承袭的上下文也就是CoroutineScope 所在的线程,而不是重新调度到其他的线程

配置协程⽇志调试环境

配置 JVM 参数 -Dkotlinx.coroutines.debug

test1.jpg

test2.jpg

引入依赖

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' // 包含了 kotlinx-coroutines-core

一、协程的基本使用

1.1 创建一个协程

在 GlobalScope 中启动的活动协程并不会使进程保活。它们就像守护线程。

// 全部示例采用自定义输出格式 -> 时间 [线程名 协程名] 内容
val formatTime = SimpleDateFormat("HH:mm:ss SSS", Locale.CHINA)
fun log(msg: String) = println("${formatTime.format(System.currentTimeMillis())} [${Thread.currentThread().name}] $msg")

fun main() {
    GlobalScope.launch {
        // delay 是一个特殊的挂起函数 ,它不会造成线程阻塞,但是会挂起协程,并且只能在协程中使用
        delay(1000L)
        log("World!")
        delay(5000L)
        log("End") // 这个不会执行
    }

    log("Hello,")
    // 协程已在等待时主线程还在继续,阻塞主线程2秒钟来保证JVM存活
    Thread.sleep(2000L)
}

输出结果

14:23:35 072 [main] Hello,
14:23:36 041 [DefaultDispatcher-worker-1 @coroutine#1] World!

1.2 桥接阻塞与⾮阻塞

runBlocking 协程构建器:调用了 runBlocking 的主线程会一直阻塞直到 runBlocking 内部的协程执行完毕

fun main() {
    GlobalScope.launch {
        delay(1000L)
        log("World!")
    }
    log("Hello, ")
    // 这个表达式阻塞了主线程,我们延迟 2 秒来保证 JVM 的存活
    runBlocking {
        delay(2000L)
    }
    log("End")
    // 这些代码只使用了非阻塞的函数 delay
}

输出结果

14:57:00 253 [main] Hello, 
14:57:01 203 [DefaultDispatcher-worker-1 @coroutine#1] World!
14:57:02 256 [main] End

1.3 结构化并发

  • 我们使用 runBlocking 协程构建器将 main 函数转换为协程。
  • 包括 runBlocking 在内的每个协程构建器都将 CoroutineScope 的实例添加到其代码块所在的作用域中。
  • 我们可以在这个作用域中启动协程而无需显式 join ,因为外部协程(示例中的 runBlocking)直到在其作用域中启动的所有协程都执行完毕后才会结束。
fun main() = runBlocking{
    launch {
        delay(1000L)
        log("World!")
    }
    log("Hello,")
}

输出结果

15:11:43 994 [main @coroutine#1] Hello,
15:11:45 008 [main @coroutine#2] World!

1.4 等待作业

延迟一段时间来等待另一个协程运行并不是一个好的选择。 让我们显式(以非阻塞方式)等待所启动的后台 Job 执行结束。

fun main() = runBlocking { //开始执行主协程
    val job = GlobalScope.launch {
        delay(1000L)
        log("World!")
    }
    log("Hello,")
    job.join() // 等待直到子协程执行结束
    //....
}

输出结果

14:59:12 203 [main @coroutine#1] Hello,
14:59:13 164 [DefaultDispatcher-worker-1 @coroutine#2] World!

1.5 作用域构建器

除了由不同的构建器提供协程作用域之外,还可以使用 coroutineScope 构建器声明自己的作用域。 它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。

runBlocking 与 coroutineScope 可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束。 主要区别在于,runBlocking 方法会阻塞当前线程来等待,而 coroutineScope 只是挂起,会释放底层线程用于其他用途。 由于存在这点差异,runBlocking 是常规函数,而 coroutineScope 是挂起函数。

fun main() = runBlocking {
    launch {
        delay(200L)
        log("Task from runBlocking")
    }
    // 创建一个协程作用域
    coroutineScope {
        launch {
            delay(500L)
            log("Task from nested launch")
        }
        delay(100L)
        log("Task from coroutine scope")
    }
    log("Coroutine scope is over")
}

输出结果:

15:24:28 231 [main @coroutine#1] Task from coroutine scope
15:24:28 284 [main @coroutine#2] Task from runBlocking
15:24:28 583 [main @coroutine#3] Task from nested launch
15:24:28 584 [main @coroutine#1] Coroutine scope is over

1.6 提取函数重构

在协程内部可以像普通函数⼀样使⽤挂 起函数, 不过其额外特性是,同样可以使⽤其他挂起函数(如本例中的 delay )来挂起协程的执⾏。

suspend fun doWorld() {
    delay(1000L)
    log("World!")
}

fun main() = runBlocking {
    launch { doWorld() }
    log("Hello,")
}

输出结果

15:30:08 632 [main @coroutine#1] Hello,
15:30:09 648 [main @coroutine#2] World!

1.7 协程很轻量

它启动了100000个协程,并且在 5 秒钟后,每个协程都输出一个点

fun main() = runBlocking {
    repeat(100000){
        launch {
            delay(5000L)
            log(".")
        }
    }
}

输出结果

15:33:17 471 [main @coroutine#99998] .
15:33:17 471 [main @coroutine#99999] .
15:33:17 471 [main @coroutine#100000] .
15:33:17 471 [main @coroutine#100001] .

通过结果我们可以知道这十万个协程都在同一个线程内,这就是协程轻量级的原因

二、取消与超时

2.1 取消协程的执⾏

所有 kotlinx.coroutines 中的挂起函数都是可被取消的,且会抛出 CancellationException。 我们没有在控制台上看到堆栈跟踪信息的打印, 这是因为在被取消的协程中 CancellationException 被认为是协程执⾏结束的正常原因。

fun main() = runBlocking {
    val job = launch {
        repeat(1000){ i ->
            delay(500L)
            log("job:I'm sleeping $i")
        }
    }
    delay(1300L)
    log("main: I'm tired of waiting!")
    //⽐如说,⼀个⽤⼾也许关闭了⼀个启动了协程的界⾯,那么现在协程的执⾏结果已经不再被需要了,这时它应该是可以被取消的
    job.cancel() // 取消协程
    job.join() // 等待结果
    log("main:Now I can quit!")
}

输出结果

16:13:27 294 [main @coroutine#2] job:I'm sleeping 0
16:13:27 796 [main @coroutine#2] job:I'm sleeping 1
16:13:28 040 [main @coroutine#1] main: I'm tired of waiting!
16:13:28 101 [main @coroutine#1] main:Now I can quit!

2.2 无法取消的协程?

如果一个协程里只有正在执⾏的计算任务没有挂起函数,那么它是不能被 job.cancel() 取消的

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch {
        var nextPrintTime = startTime
        var i = 0
        //这里没有挂起函数
        while(i < 5){  // ⼀个执⾏计算的循环,只是为了占⽤ CPU
            if(System.currentTimeMillis() >= nextPrintTime){
                println("job:I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并且等待它结束
    println("main:Now I can quit.")
}

输出结果

16:19:20 176 [main @coroutine#2] job:I'm sleeping 0 ...
16:19:20 601 [main @coroutine#2] job:I'm sleeping 1 ...
16:19:21 101 [main @coroutine#2] job:I'm sleeping 2 ...
16:19:21 601 [main @coroutine#2] job:I'm sleeping 3 ...
16:19:22 101 [main @coroutine#2] job:I'm sleeping 4 ...
16:19:22 102 [main @coroutine#1] main:I'm tired of waiting!
16:19:22 104 [main @coroutine#1] main:Now I can quit.

照常输出计算逻辑,取消失败

2.3 使计算代码协程可取消

通过显式的检查取消状态,来实现取消 Job

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        //isActive 是⼀个可以被使⽤在 CoroutineScope 中的扩展属性
        while(isActive){
            if(System.currentTimeMillis() >= nextPrintTime){
                log("job:I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    log("main:I'm tired of waiting!")
    job.cancelAndJoin()
    log("main:Now I can quit.")
}

输出结果

16:45:37 783 [DefaultDispatcher-worker-1 @coroutine#2] job:I'm sleeping 0 ...
16:45:38 218 [DefaultDispatcher-worker-1 @coroutine#2] job:I'm sleeping 1 ...
16:45:38 718 [DefaultDispatcher-worker-1 @coroutine#2] job:I'm sleeping 2 ...
16:45:39 044 [main @coroutine#1] main:I'm tired of waiting!
16:45:39 047 [main @coroutine#1] main:Now I can quit.

2.4 在 finally 中释放资源

finally 代码块中使用挂起函数也会被 job.cancelAndJoin() 取消

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                log("job:I'm sleeping $i ...")
                delay(500L)
                //假如在这里释放资源
            }
        } finally {
            // 在这里释放资源
            // 任何尝试在 finally 块中调⽤挂起函数的⾏为都会抛出 CancellationException
            // 注意这⾥运⾏的挂起函数也会被 job.cancelAndJoin() 取消
            log("job:I'm running finally")
        }
    }
    delay(1300L)
    log("main:I'm tired of waiting!")
    job.cancelAndJoin()
    log("main:Now I can quit.")
}

输出结果

17:02:49 386 [main @coroutine#2] job:I'm sleeping 0 ...
17:02:49 891 [main @coroutine#2] job:I'm sleeping 1 ...
17:02:50 396 [main @coroutine#2] job:I'm sleeping 2 ...
17:02:50 640 [main @coroutine#1] main:I'm tired of waiting!
17:02:50 703 [main @coroutine#2] job:I'm running finally
17:02:50 705 [main @coroutine#1] main:Now I can quit.

2.5 运⾏不能取消的代码块

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job:I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable){
                println("job:I'm running finally")
                delay(2000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L)
    println("main:I'm tired of waiting!")
    job.cancelAndJoin()
    println("main:Now I can quit.")
}

输出结果

17:22:01 471 [main @coroutine#2] job:I'm sleeping 0 ...
17:22:01 978 [main @coroutine#2] job:I'm sleeping 1 ...
17:22:02 479 [main @coroutine#2] job:I'm sleeping 2 ...
17:22:02 697 [main @coroutine#1] main:I'm tired of waiting!
17:22:02 764 [main @coroutine#2] job:I'm running finally
17:22:04 768 [main @coroutine#2] job: And I've just delayed for 1 sec because I'm non-cancellable
17:22:04 769 [main @coroutine#1] main:Now I can quit.

finally 中 withContext 函数以及 NonCancellable 上下文代码块里的挂起函数不受 job.cancelAndJoin 影响照常执行

2.6 超时

withTimeoutOrNull 通过返回 null 来进⾏超时操作,从⽽替代抛出⼀个异常

fun main() = runBlocking {
    val result = withTimeoutOrNull(1330L){
        repeat(1000){ i ->
            println("I'm sleeping $i")
            delay(500L)
        }
        "OK"
    }
    println(result ?: "Done")
}

输出结果

17:30:20 264 [main @coroutine#1] I'm sleeping 0
17:30:20 775 [main @coroutine#1] I'm sleeping 1
17:30:21 279 [main @coroutine#1] I'm sleeping 2
17:30:21 605 [main @coroutine#1] Done

三、组合挂起函数

3.1 默认顺序调⽤

// 挂起函数
suspend fun doSomethingUsefulOne(): Int {
    println("doSomethingUsefulOne")
    delay(1000L)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    println("doSomethingUsefulTwo")
    delay(1000L)
    return 29
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

输出结果

17:36:12 322 [main @coroutine#1] doSomethingUsefulOne
17:36:13 331 [main @coroutine#1] doSomethingUsefulTwo
17:36:14 335 [main @coroutine#1] The answer is 42
17:36:14 335 [main @coroutine#1] Completed in 2059 ms

通过输出发现是同一个线程,且同一个协程,执行时间 2059 ms

3.2 使⽤ async 并发

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        log("The answer is ${one.await() + two.await()}")
    }
    //这⾥快了两倍,因为两个协程并发执⾏。请注意使⽤协程进⾏并发总是显式的
    log("Completed in $time ms")
}

输出结果

17:44:56 194 [main @coroutine#2] doSomethingUsefulOne
17:44:56 198 [main @coroutine#3] doSomethingUsefulTwo
17:44:57 199 [main @coroutine#1] The answer is 42
17:44:57 200 [main @coroutine#1] Completed in 1068 ms

通过输出对比 3.1 可以发现是同一个线程,不同协程,执行时间1068 ms。async 只是启动了一个协程, 所以如果执行的是 CPU 密集型任务,那么执行时间依旧会是 2059 ms左右,CPU 密集型任务解决方案参考:3.4 async ⻛格的函数

3.3 惰性启动的 async

fun main() = runBlocking {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
        val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        // 执⾏⼀些计算
        // 启动第⼀个
        one.start()
        // 启动第⼆个
        two.start()
        log("The answer is ${one.await() + two.await()}")
    }
    log("Completed in $time ms")
}

输出结果

13:19:06 284 [main @coroutine#2] doSomethingUsefulOne
13:19:06 286 [main @coroutine#3] doSomethingUsefulTwo
13:19:07 289 [main @coroutine#1] The answer is 42
13:19:07 289 [main @coroutine#1] Completed in 1057 ms

3.4 async ⻛格的函数

fun doSomethingUseful(long: Long = 1000L) {
    var nextPrintTime = System.currentTimeMillis()
    // 模拟计算耗时
    while (true) {
        var currentTime = System.currentTimeMillis()
        if (currentTime - long >= nextPrintTime) {
            break
        }
    }
}

// 返回值类型是 Deferred<Int>
fun doSomethingUsefulOneAsync() = GlobalScope.async {
    log("doSomethingUsefulOneAsync")
    doSomethingUseful()
    20
}

fun doSomethingUsefulTwoAsync() = GlobalScope.async {
    log("doSomethingUsefulTwoAsync")
    doSomethingUseful(900L)
    10
}

fun main() {
    val time = measureTimeMillis {
        // 我们可以在协程作用域外启动异步执⾏
        val one = doSomethingUsefulOneAsync()
        val two = doSomethingUsefulTwoAsync()
        // 当我们等待结果的时候,这⾥我们使⽤ `runBlocking { …… }` 来阻塞主线程等待结果
        runBlocking {
            log("The answer is ${one.await() + two.await()}")
        }
    }
    log("Completed in $time ms")
}

输出结果

13:45:09 404 [DefaultDispatcher-worker-2 @coroutine#1] doSomethingUsefulOneAsync
13:45:09 404 [DefaultDispatcher-worker-1 @coroutine#2] doSomethingUsefulTwoAsync
13:45:10 406 [main @coroutine#3] The answer is 30
13:45:10 406 [main] Completed in 1115 ms

CPU 密集型任务分别在不同线程,且不同协程中执行

3.5 使⽤ async 的结构化并发

如果其中⼀个⼦协程(即 two )失败,第⼀个 async 以及等待中的⽗协程都会被取消

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch (e: ArithmeticException) {
        log("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum() = coroutineScope {
    val one = async {
        try {
            delay(Long.MAX_VALUE)
            42
        } finally {
            log("First child was cancelled")
        }
    }
    val two = async<Int> {
        log("Second child throw an Exception.")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

输出结果

14:06:36 082 [main @coroutine#3] Second child throw an Exception.
14:06:36 132 [main @coroutine#2] First child was cancelled
14:06:36 144 [main @coroutine#1] Computation failed with ArithmeticException

四、协程上下⽂与调度器

4.1 调度器与线程

协程总是运⾏在⼀些以 CoroutineContext 类型为代表的上下⽂中,协程上下⽂是各种不同元素的集合,其中主元素是协程中的 Job

所有的协程构建器诸如 launch 和 async 接收⼀个可选的 CoroutineContext 参数,它可以被⽤来显式的为⼀ 个新协程或其它上下⽂元素指定⼀个调度器

fun main() = runBlocking<Unit> {

    // 当调⽤ launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下⽂(以及调度器)
    // 在这 个案例中,它从 main 线程中的 runBlocking 主协程承袭了上下⽂
    // CoroutineName 命名协程以⽤于调试
    val job = launch(CoroutineName("v1coroutine")) {
        delay(1000)
        log("main runBlocking")
        log("My job is ${coroutineContext.job}")
    }

    // ⾮受限的调度器⾮常适⽤于执⾏不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如 UI )的协程
    launch(Dispatchers.Unconfined + CoroutineName("test")) {
        log("Unconfined")
    }

    // 将会获取默认调度器
    // 默认调度器使⽤共享的后台线程池,线程数和 CPU 内核数相关
    // 适合执行 CPU 密集型任务
    launch(Dispatchers.Default) {
        log("Default")
    }

    // 将会获取默认调度器
    // 默认调度器使⽤共享的后台线程池,线程数默认限制为 64 个线程
    // 适合执行磁盘或网络 I/O 密集型任务
    launch(Dispatchers.IO) {
        log("IO")
    }

    // 将使它获得⼀个新的线程
    // ⼀个专⽤的线程是⼀种⾮常昂贵的资源
    launch(newSingleThreadContext("MyOwnThread")) {
        log("newSingleThreadContext")
    }
}

输出结果

16:11:49 670 [main @test#3] Unconfined
16:11:49 680 [DefaultDispatcher-worker-1 @coroutine#4] Default
16:11:49 681 [DefaultDispatcher-worker-2 @coroutine#5] IO
16:11:49 689 [MyOwnThread @coroutine#6] newSingleThreadContext
16:11:50 694 [main @v1coroutine#2] main runBlocking
16:11:50 697 [main @v1coroutine#2] My job is "v1coroutine#2":StandaloneCoroutine{Active}@6d3af739

4.2 ⾮受限调度器 vs 受限调度器

fun main() = runBlocking<Unit> {
    // 协程可以在⼀个线程上挂起并在其它线程上恢复
    // 不适合更新UI
    launch(Dispatchers.Unconfined) {
        log("Unconfined : before")
        delay(500L)
        // 挂起之后,这里是子线程
        log("Unconfined : After delay")
    }

    launch {
        log("main runBlocking: before")
        delay(1000L)
        log("main runBlocking: After delay")
    }
}

输出结果

14:22:19 557 [main @coroutine#2] Unconfined : before
14:22:19 570 [main @coroutine#3] main runBlocking: before
14:22:20 066 [kotlinx.coroutines.DefaultExecutor @coroutine#2] Unconfined : After delay
14:22:20 571 [main @coroutine#3] main runBlocking: After delay

4.3 ⼦协程

fun main() = runBlocking {
    // 当⼀个⽗协程被取消的时候,所有它的⼦协程也会被递归的取消
    val request = launch {
        // 孵化了两个⼦作业, 其中⼀个通过 GlobalScope 启动
        // 当使⽤ GlobalScope 来启动⼀个协程时,则新协程的作业没有⽗作业
        // 因此它与这个启动的作⽤域⽆关 且独⽴运作
        GlobalScope.launch {
            log("job1: I run in GlobalScope and execute independently!")
            delay(1000L)
            log("job1: I am not affected by cancellation of the request")
        }

        launch {
            delay(100L)
            log("job2: I am a child of the request coroutine")
            delay(1000L)
            log("job2: I will not execute this line if my parent request is cancelled")
        }
    }

    delay(500L)
    request.cancel()
    delay(1000L)
    log("main: Who has survived request cancellation?")
}

输出结果

14:10:53 141 [DefaultDispatcher-worker-1 @coroutine#3] job1: I run in GlobalScope and execute independently!
14:10:53 249 [main @coroutine#4] job2: I am a child of the request coroutine
14:10:54 148 [DefaultDispatcher-worker-1 @coroutine#3] job1: I am not affected by cancellation of the request
14:10:54 633 [main @coroutine#1] main: Who has survived request cancellation?

4.4 父协程的职责

fun main() = runBlocking {
    val request = launch {
        repeat(3) { i ->
            launch {
                delay((i + 1) * 200L)
                log("Coroutine $i is done")
            }
        }
        log("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() //⼀个⽗协程总是等待所有的⼦协程执⾏结束
    log("Now processing of the request is complete")
}

输出结果

14:12:40 452 [main @coroutine#2] request: I'm done and I don't explicitly join my children that are still active
14:12:40 659 [main @coroutine#3] Coroutine 0 is done
14:12:40 860 [main @coroutine#4] Coroutine 1 is done
14:12:41 060 [main @coroutine#5] Coroutine 2 is done
14:12:41 061 [main @coroutine#1] Now processing of the request is complete

五、参考链接