1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > Kotlin协程-Coroutines-原汁原味一篇就够了系列

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

时间:2023-08-19 05:05:47

相关推荐

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

文章目录

Kotlin协程-Coroutines1. 协程概述1.1 来自官方的解释:[Coroutines Guide - Kotlin Programming Language](/docs/reference/coroutines/coroutines-guide.html)1.2 Table of contents2. 协程基础(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开发者可以轻松上手

1. 协程概述

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

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

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

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

1.2 Table of contents

Coroutine basicsCancellation and timeoutsComposing suspending functionsCoroutine context and dispatchersException handling and supervisionChannels (experimental)Shared mutable state and concurrencySelect expression (experimental)

2. 协程基础(Coroutine basics)

2.1 第一个协程程序

/** Copyright - 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.basic01import kotlinx.coroutines.*fun main() {GlobalScope.launch {// launch new coroutine in background and continuedelay(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 delayedThread.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 - 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.basic02import kotlinx.coroutines.*fun main() {GlobalScope.launch {// launch new coroutine in background and continuedelay(1000L)println("World!")}println("Hello,") // main thread continues here immediatelyrunBlocking {// but this expression blocks the main threaddelay(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 - 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.basic02bimport kotlinx.coroutines.*fun main() = runBlocking<Unit> {// start main coroutineGlobalScope.launch {// launch new coroutine in background and continuedelay(1000L)println("World!")}println("Hello,") // main coroutine continues here immediatelydelay(2000L)// delaying for 2 seconds to keep JVM alive}

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

运行结果:

Hello,World!

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

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

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

2.3 等待完成(Waiting for a job)

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

/** Copyright - 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.basic03import kotlinx.coroutines.*fun main() = runBlocking {//sampleStartval job = GlobalScope.launch {// launch new coroutine and keep a reference to its Jobdelay(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 - 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.basic03simport kotlinx.coroutines.*fun main() = runBlocking {// this: CoroutineScopelaunch {// launch new coroutine in the scope of runBlockingdelay(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 - 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.basic04import kotlinx.coroutines.*fun main() = runBlocking {// this: CoroutineScopelaunch {delay(200L)println("Task from runBlocking")}coroutineScope {// Creates a new coroutine scopelaunch {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 scopeTask from runBlockingTask from nested launchCoroutine scope is over

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

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

/** Copyright - 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.basic05import kotlinx.coroutines.*fun main() = runBlocking {launch {doWorld() }println("Hello,")}// this is your first suspending functionsuspend 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 coroutineslaunch {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 - 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.basic07import kotlinx.coroutines.*fun main() = runBlocking {//sampleStartGlobalScope.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.")}

代码出处: (/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 = startTimevar i = 0while (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.")}

代码出处: (/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 = startTimevar i = 0while (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.")}

代码出处: (/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.")}

代码出处: (/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 finallymain: 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.")}

代码出处: (/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 finallyAnd I've just delayed for 1 sec because I'm non-cancellablemain: 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)}}}

代码出处: (/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")}

代码出处: (/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

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。