Kotlin协程-Coroutines-原汁原味一篇就够了系列

文章目录

  • Kotlin协程-Coroutines
    • 1. 协程概述
      • 1.1 来自官方的解释:[Coroutines Guide - Kotlin Programming Language](https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html)
      • 1.2 Table of contents
    • 2. 协程基础(Coroutine basics)
      • 2.1 第一个**协程**程序
      • 2.2 连接阻塞和非阻塞世界
      • 2.3 等待完成(Waiting for a job)
      • 2.4 结构化并发(Structured concurrency)
      • 2.5 范围构建器(Scope builder)
      • 2.6 重构和提取方法(Extract function refactoring)
      • 2.7 协程是轻量级的(Coroutines ARE light-weight)
      • 2.8 全局协程就像是守护线程一样(Global coroutines are like daemon threads)
    • 3. 取消与超时
      • 3.1 取消协程的执行
      • 3.2 取消是协作的
      • 3.3 使计算代码可取消
      • 3.4 在 finally 中释放资源
      • 3.5 运行不能取消的代码块
      • 3.6超时

Kotlin协程-Coroutines

本篇是对官方文档的翻译,中间有demo查看,需要android studio配合,Android开发者可以轻松上手
Kotlin协程-Coroutines-原汁原味一篇就够了系列_第1张图片

1. 协程概述

1.1 来自官方的解释:Coroutines Guide - Kotlin Programming Language

Kotlin作为一种语言,在其标准库中仅提供最小的低级API,以使各种其他库能够使用协程。与许多具有类似功能的其他语言一样,asyncawait不是Kotlin中的关键字,甚至不是其标准库的一部分。此外,Kotlin的暂停函数概念为异步操作提供了比 futures 和 promises更安全且更不易出错的抽象。kotlinx.coroutines是由JetBrains开发的丰富的协程库。它包含本指南涵盖的许多高级协程启用的原函数,包括启动,异步等

这是关于kotlinx.coroutines的核心功能的指南,其中包含一系列示例,分为不同的主题。

为了使用协程以及遵循本指南中的示例,您需要像kotlinx.coroutines/README.md中解释的那样在kotlinx-coroutines-core模块中添加依赖项。

1.2 Table of contents

  • Coroutine basics
  • Cancellation and timeouts
  • Composing suspending functions
  • Coroutine context and dispatchers
  • Exception handling and supervision
  • Channels (experimental)
  • Shared mutable state and concurrency
  • Select expression (experimental)

2. 协程基础(Coroutine basics)

2.1 第一个协程程序

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic01

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

代码出处:kotlinx.coroutines/example-basic-01.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Hello,
World!

从本质上讲,协同程序是轻量级的线程。它们是在CoroutineScope(协程器)的上下文中与启动协程构建器一起创建的。在这里,我们将在GlobalScope(全局协程器)中启动一个新的协程,这意味着新协程的生命周期仅受整个应用程序的生命周期的限制。

您可以使用Thread.sleep(...)替换GlobalScope.launch {...}Thread.sleep{...}以及delay(...)。试试吧。

如果您首先将Thread替换GlobalScope.launch,编译器将生成以下错误:

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

这是因为delay是一个特殊的挂起函数,它不会阻塞一个线程(Thread),但会暂停coroutine,它只能在一个协程中使用。

2.2 连接阻塞和非阻塞世界

第一个示例在同一代码中混合非阻塞delay(...)和阻塞Thread.sleep(...)。很容易忘记哪一个阻塞,哪一个没有阻塞。让我们明确一下使用runBlocking 协程构建器进行阻塞:

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic02

import kotlinx.coroutines.*

fun main() { 
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

代码出处:kotlinx.coroutines/example-basic-02.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Hello,
World!

结果是相同的,但此代码仅使用非阻塞delay。调用runBlocking的主线程阻塞,直到runBlocking内的协程完成。

这个例子也可以用更惯用的方式重写,使用runBlocking来包main函数的执行:

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic02b

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

代码出处: kotlinx.coroutines/example-basic-02b.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Hello,
World!

这里runBlocking {...}作为适配器,用于启动顶级主协程。我们明确指定了它的Unit返回类型,因为Kotlin中格式良好的main函数必须返回Unit

这也是一种为挂起函数编写单元测试的方法:

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // here we can use suspending functions using any assertion style that we like
    }
}

2.3 等待完成(Waiting for a job)

在另一个协程正在工作时延迟一段时间并不是一个好方法。让我们明确等待(以非阻塞方式),直到我们启动的后台作业完成:


/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic03

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = GlobalScope.launch { // launch new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
//sampleEnd    
}

代码出处: kotlinx.coroutines/example-basic-03.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Hello,
World!

现在结果仍然相同,但主协程的代码不以任何方式与后台作业的持续时间相关联。好多了。

2.4 结构化并发(Structured concurrency)

对于协程的实际使用仍有一些期望。当我们使用GlobalScope.launch时,我们创建了一个顶级协程。尽管它很轻,但它在运行时仍会消耗一些内存资源。如果我们忘记保留对新启动的协程的引用,它仍会运行。如果协程中的代码挂起(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并且内存不足会怎么样?必须手动保持对所有已启动的协程的引用并join它们是容易出错的.

有一个更好的解决方案。我们可以在代码中使用结构化并发。就像我们通常使用线程(线程总是全局的)一样,我们可以在我们正在执行的操作的特定范围内启动协程,而不是在GlobalScope中启动协程

在我们的示例中,我们使用runBlocking 协程构建器main函数转换为协程。每个协程构建器(包括runBlocking)都将CoroutineScope的实例添加到其代码块的范围内。我们可以在此范围内启动协程,而无需显式join它们,因为在其范围内启动的所有协程完成之前,外部协程(在我们的示例中为runBlocking)不会完成。

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic03s

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

代码出处: kotlinx.coroutines/example-basic-03s.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Hello,
World!

2.5 范围构建器(Scope builder)

除了由不同构建器提供的协同作用域之外,还可以使用coroutineScope构建器声明自己的作用域。它会创建新的协程范围,并且在所有已启动的子项完成之前不会完成。 runBlockingcoroutineScope之间的主要区别在于后者在等待所有子项完成时不会阻塞当前线程。

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic04

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

代码出处: kotlinx.coroutines/example-basic-04.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

2.6 重构和提取方法(Extract function refactoring)

让我们将launch {...}中的代码块提取到一个单独的函数中。当您对此代码执行“提取功能”重构时,您将获得一个带有suspend修饰符的新函数。这是你的第一个挂起函数挂起函数可以在协程内部使用,就像常规函数一样,但它们的附加功能是它们可以反过来使用其他挂起函数(如本例中的delay)来暂停执行协程。

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic05

import kotlinx.coroutines.*

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

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

代码出处:kotlinx.coroutines/example-basic-05.kt at master · Kotlin/kotlinx.coroutines

运行结果:

Hello,
World!

但是,如果提取的函数包含了在当前作用域上调用的协程构建器,该怎么办?在这种情况下,提取函数上的suspend修饰符是不够的。在CoroutineScope上做一个doWorld扩展方法是其中一种解决方案,但它并不总是适用,因为它不会使API更清晰。惯用解决方案是将显式CoroutineScope作为包含目标函数的类中的字段,或者在外部类实现CoroutineScope时隐式。作为最后的手段,可以使用CoroutineScope(coroutineContext),但是这种方法在结构上是不安全的,因为您不再能够控制此方法的执行范围。只有私有API才能使用此构建器。

2.7 协程是轻量级的(Coroutines ARE light-weight)

运行以下代码:

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(1000L)
            print(".")
        }
    }
}

代码出处: kotlinx.coroutines/example-basic-06.kt at master · Kotlin/kotlinx.coroutines

运行结果: 省略(请使用IntelliJ IDEA自行测试)

它启动了10万个协程,一秒钟之后,每个协程都打印出一个点。现在,尝试使用线程。会发生什么? (很可能你的代码会产生某种内存不足的错误)

2.8 全局协程就像是守护线程一样(Global coroutines are like daemon threads)

下面的代码在GlobalScope中启动一个长时间运行的协程,它打印“我正在睡觉”每秒两次,然后在一段延迟后从主函数返回:

/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
package kotlinx.coroutines.guide.basic07

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // just quit after delay
//sampleEnd    
}

代码出处: kotlinx.coroutines/example-basic-07.kt at master · Kotlin/kotlinx.coroutines

运行结果:


I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
省略(请使用IntelliJ IDEA自行测试)

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

3. 取消与超时

这一部分包含了协程的取消与超时。

*`取消与超时](#取消与超时)

  • 取消协程的执行
  • 取消是协作的
  • 使计算代码可取消
  • 在 finally 中释放资源
  • 运行不能取消的代码块
  • 超时

3.1 取消协程的执行

在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。
比如说,一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果
已经不再被需要了,这时,它应该是可以被取消的。
该launch 函数返回了一个可以被用来取消运行中的协程的Job:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val job = launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancel() // 取消该任务
    job.join() // 等待任务执行结束
    println("main: Now I can quit.")

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-01.kt)

程序执行后的输出如下:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

一旦 main 函数调用了job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了。
这里也有一个可以使Job 挂起的函数cancelAndJoin
它合并了对cancelJob.cancel 以及joinJob.join 的调用。

3.2 取消是协作的

协程的取消是 协作 的。一段协程代码必须协作才能被取消。
所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消,
并在取消时抛出CancellationException。 然而,如果协程正在执行
计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例
代码所示:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消一个任务并且等待它结束
    println("main: Now I can quit.")

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-02.kt)

运行示例代码,并且我们可以看到它连续打印出了“I’m sleeping” ,甚至在调用取消后,
任务仍然执行了五次循环迭代并运行到了它结束为止。

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm sleeping 3 ...
I'm sleeping 4 ...
main: Now I can quit.

3.3 使计算代码可取消

我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期
调用挂起函数来检查取消。对于这种目的yield 是一个好的选择。
另一种方法是显式的检查取消状态。让我们试试第二种方法。

将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。

import kotlinx.coroutines.*

fun main() = runBlocking {

    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 可以被取消的计算循环
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该任务并等待它结束
    println("main: Now I can quit.")

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-03.kt)

你可以看到,现在循环被取消了。isActive 是一个可以被使用在
CoroutineScope 中的扩展属性。

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

3.4 在 finally 中释放资源

我们通常使用如下的方法处理在被取消时抛出CancellationException 的可被取消
的挂起函数。比如说,try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候
执行它们的终结动作:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val job = launch {
        try {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("I'm running finally")
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该任务并且等待它结束
    println("main: Now I can quit.")

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-04.kt)

join 和cancelAndJoin 等待了所有的终结动作执行完毕,
所以运行示例得到了下面的输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
main: Now I can quit.

3.5 运行不能取消的代码块

在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出
CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个
问题,所有良好的关闭操作(关闭一个文件、取消一个任务、或是关闭任何一种
通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在
真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在
withContext(NonCancellable) {……} 中,并使用withContext 函数以及NonCancellable 上下文,见如下示例所示:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val job = launch {
        try {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("I'm running finally")
                delay(1000L)
                println("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.")

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-05.kt)

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.

3.6超时

在实践中绝大多数取消一个协程的理由是
它有可能超时。
当你手动追踪一个相关Job 的引用并启动了一个单独的协程在
延迟后取消追踪,这里已经准备好使用withTimeout 函数来做这件事。
来看看示例代码:

import kotlinx.coroutines.*

fun main() = runBlocking {

    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-06.kt)

运行后得到如下输出:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

withTimeout 抛出了 TimeoutCancellationException,它是CancellationException的子类。
我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为
在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。
然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout。

由于取消只是一个例外,所有的资源都使用常用的方法来关闭。
如果你需要做一些各类使用超时的特别的额外操作,可以使用类似withTimeout
的withTimeoutOrNull函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...}
代码块中,而withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // 在它运行得到结果之前取消它
    }
    println("Result is $result")

}

代码出处: (https://github.com/hltj/kotlinx.coroutines-cn/blob/master/kotlinx-coroutines-core/jvm/test/guide/example-cancel-07.kt)

运行这段代码时不再抛出异常:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

你可能感兴趣的:(Android基础,Kotlin)