Kotlin使用挂起函数为异步操作,使用kotlinx.coroutines中的launch、async

1. 第⼀个协程程序

import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动⼀个新的协程并继续
delay(1000L) // ⾮阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

  代码运行的结果

Hello,
World!

  本质上,协程是轻量级的线程。它们在某些 CoroutineScope 上下⽂中与 launch 协程构建器 ⼀起启 动。这⾥我们在 GlobalScope 中启动了⼀个新的协程,这意味着新协程的⽣命周期只受整个应⽤程序 的⽣命周期限制。 可以将 GlobalScope.launch { …… } 替换为 thread { …… } ,并将 delay(……) 替换为 Thread.sleep(……) 达到同样⽬的。试试看(不要忘记导⼊ kotlin.concurrent.thread )。 — — — — — — — — — 协程基础 第⼀个协程程序 205 如果你⾸先将 GlobalScope.launch 替换为 thread ,编译器会报以下错误:

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

  这是因为 delay 是⼀个特殊的 挂起函数 ,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中 使⽤。

2. 桥接阻塞与⾮阻塞的世界

  第⼀个⽰例在同⼀段代码中混⽤了 ⾮阻塞的 delay(……) 与 阻塞的 Thread.sleep(……) 。这容易 让我们记混哪个是阻塞的、哪个是⾮阻塞的。让我们显式使⽤ runBlocking 协程构建器来阻塞:

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

  结果是相似的,但是这些代码只使⽤了⾮阻塞的函数 delay。调⽤了 runBlocking 的主线程会⼀直 阻塞 直到 runBlocking 内部的协程执⾏完毕。

  这个⽰例可以使⽤更合乎惯⽤法的⽅式重写,使⽤ runBlocking 来包装 main 函数的执⾏:

import kotlinx.coroutines.*
fun main() = runBlocking<Unit> { // 开始执⾏主协程
GlobalScope.launch { // 在后台启动⼀个新的协程并继续
delay(1000L)
println("World!")
}
println("Hello,") // 主协程在这⾥会⽴即执⾏
delay(2000L) // 延迟 2 秒来保证 JVM 存活
}

  这⾥的 runBlocking { …… } 作为⽤来启动顶层主协程的适配器。我们显式指定了其返回 类型 Unit,因为在 Kotlin 中 main 函数必须返回 Unit 类型。

  这也是为挂起函数编写单元测试的⼀种⽅式:

class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
    // 这⾥我们可以使⽤任何喜欢的断⾔⻛格来使⽤挂起函数
}
}

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

val job = GlobalScope.launch { // 启动⼀个新协程并保持对这个作业的引⽤
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // 等待直到⼦协程执⾏结束

  现在,结果仍然相同,但是主协程与后台作业的持续时间没有任何关系了。好多了。

3. 结构化的并发

  协程的实际使⽤还有⼀些需要改进的地⽅。当我们使⽤ GlobalScope.launch 时,我们会创建⼀个 顶层协程。虽然它很轻量,但它运⾏时仍会消耗⼀些内存资源。如果我们忘记保持对新启动的协程的引 ⽤,它还会继续运⾏。如果协程中的代码挂起了会怎么样(例如,我们错误地延迟了太⻓时间),如果我们 启动了太多的协程并导致内存不⾜会怎么样?必须⼿动保持对所有已启动协程的引⽤并 join 之很容易 出错。 有⼀个更好的解决办法。我们可以在代码中使⽤结构化并发。我们可以在执⾏操作所在的指定作⽤域内 启动协程,⽽不是像通常使⽤线程(线程总是全局的)那样在 GlobalScope 中启动。 在我们的⽰例中,我们使⽤ runBlocking 协程构建器将 main 函数转换为协程。包括 runBlocking 在内的每个协程构建器都将 CoroutineScope 的实例添加到其代码块所在的作⽤域中。我们可以在这 个作⽤域中启动协程⽽⽆需显式 join 之,因为外部协程(⽰例中的 runBlocking )直到在其作⽤域 中启动的所有协程都执⾏完毕后才会结束。因此,可以将我们的⽰例简化为:

import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // 在 runBlocking 作⽤域中启动⼀个新协程
delay(1000L)
println("World!")
}
println("Hello,")
}

  

4. 作⽤域构建器

  除了由不同的构建器提供协程作⽤域之外,还可以使⽤ coroutineScope 构建器声明⾃⼰的作⽤域。它 会创建⼀个协程作⽤域并且在所有已启动⼦协程执⾏完毕之前不会结束。 runBlocking 与 coroutineScope 可能看起来很类似,因为它们都会等待其协程体以及所有⼦协程结 束。主要区别在于,runBlocking ⽅法会阻塞当前线程来等待,⽽ coroutineScope 只是挂起,会释放底 层线程⽤于其他⽤途。由于存在这点差异,runBlocking 是常规函数,⽽ coroutineScope 是挂起函数。 可以通过以下⽰例来演⽰:

import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // 创建⼀个协程作⽤域
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // 这⼀⾏会在内嵌 launch 之前输出
}
println("Coroutine scope is over") // 这⼀⾏在内嵌 launch 执⾏完毕后才输出
}

  请注意,(当等待内嵌 launch 时)紧挨“Task from coroutine scope”消息之后,就会执⾏并输出“Task from runBlocking”⸺尽管 coroutineScope 尚未结束。

5. 提取函数重构

  我们来将 launch { …… } 内部的代码块提取到独⽴的函数中。当你对这段代码执⾏“提取函数”重构 时,你会得到⼀个带有 suspend 修饰符的新函数。这是你的第⼀个挂起函数。在协程内部可以像普通 函数⼀样使⽤挂起函数,不过其额外特性是,同样可以使⽤其他挂起函数(如本例中的 delay )来挂 起协程的执⾏。

import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// 这是你的第⼀个挂起函数
suspend fun doWorld() {
delay(1000L)
println("World!")
}

  但是如果提取出的函数包含⼀个在当前作⽤域中调⽤的协程构建器的话,该怎么办?在这种情况下,所 提取函数上只有 suspend 修饰符是不够的。为 CoroutineScope 写⼀个 doWorld 扩展⽅法是其 中⼀种解决⽅案,但这可能并⾮总是适⽤,因为它并没有使 API 更加清晰。惯⽤的解决⽅案是要么显式 将 CoroutineScope 作为包含该函数的类的⼀个字段,要么当外部类实现了 CoroutineScope 时 隐式取得。作为最后的⼿段,可以使⽤ CoroutineScope(coroutineContext),不过这种⽅法结构上不安 全,因为你不能再控制该⽅法执⾏的作⽤域。只有私有 API 才能使⽤这个构建器。

6.全局协程像守护线程

  以下代码在 GlobalScope 中启动了⼀个⻓期运⾏的协程,该协程每秒输出“I'm sleeping”两次,之后在 主函数中延迟⼀段时间后返回。

GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 在延迟后退出

  你可以运⾏这个程序并看到它输出了以下三⾏后终⽌:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

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

7.取消协程的执行

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

val job = launch {
repeat(1000) { i ->
println("job: 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.")

  程序执⾏后的输出如下:

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

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

8.取消是协作的

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

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
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.")

  运⾏⽰例代码,并且我们可以看到它连续打印出了“I'm sleeping”,甚⾄在调⽤取消后,作业仍然执⾏了 五次循环迭代并运⾏到了它结束为⽌。

9.使计算代码可取消

  我们有两种⽅法来使执⾏计算的代码可以被取消。第⼀种⽅法是定期调⽤挂起函数来检查取消。对于这 种⽬的 yield 是⼀个好的选择。另⼀种⽅法是显式的检查取消状态。让我们试试第⼆种⽅法。 将前⼀个⽰例中的 while (i < 5) 替换为 while (isActive) 并重新运⾏它。

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可以被取消的计算循环
// 每秒打印消息两次
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.")

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

10. 在 finally 中释放资源

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

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

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

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

  

11.  运⾏不能取消的代码块

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

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(1000L)
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.")

  

12. 超时

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

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

  运⾏后得到如下输出:

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 来进⾏超时操作,从⽽替代抛出⼀个异常:

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

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

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

  

kotlin协程——>基础、取消与超时的更多相关文章

  1. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

  2. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

  3. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  4. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  5. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  6. Retrofit使用Kotlin协程发送请求

    Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...

  7. rxjava回调地狱-kotlin协程来帮忙

    本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...

  8. Kotlin协程通信机制: Channel

    Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...

  9. Kotlin协程重要概念详解【纯理论】

    在之前对Kotlin的反射进行了详细的学习,接下来进入一个全新的篇章,就是关于Koltin的协程[coroutine],在正式撸码之前先对它有一个全面理论化的了解: 协程的定义: 协和通过将复杂性放入 ...

  10. pyhon——进程线程、与协程基础概述

    一直以来写博客都是实用主义者,只写用法,没信心写原理,但是每一次写作业的过程都有一种掘地三尺的感觉,终于,写博客困难症重症患者经历了漫长的思想斗争,还是决定把从网上淘到的各种杂货和自己的总结放在一起, ...

随机推荐

  1. 【SpringBoot】07 探索配置方式 Part3 多环境配置

    1.按多个Profile文件来配置 SpringBoot默认会使用第一个 我们可以在默认的application.properties中设置激活哪种环境配置 profile的命名规则 2.按Yml可以 ...

  2. Hessian Free Optimization——外国网友分享的“共轭梯度”的推导

    外国网友分享的"共轭梯度"的推导: https://andrew.gibiansky.com/blog/machine-learning/hessian-free-optimiza ...

  3. Linux共享内存通信的C语言Demo代码

    重点注明: 本文代码来源于: https://blog.csdn.net/github_38294679/article/details/122360026 ===================== ...

  4. 记录一次Ubuntu20.04死机经过!!!在Ubuntu下使用Chrome的“无痕式”窗口,如果打开标签页过多就会造成死机

    这里要说的事情就是自己刚刚经历的事情,而且尝试了多次最后证明,在Ubuntu下使用Chrome的"无痕式"窗口,如果打开标签页过多就会造成死机. 如何在Ubuntu下安装Chrom ...

  5. 键盘中上、下、左、右四个光标键所对应的ASCII码值为多少

    首先给出ASCII码值表: 上.下.左.右这四个光标键对应的ASCII码值不是一个值而是三个,准确的说光标键的ASCII码值是一个组合. 每个方向键所对应的三个键值为:0x1b + 0x5b + n ...

  6. java中线程的6中状态

    1.背景 编写多线程相关的程序,必然会用到线程状态的相关知识点, 那么这篇博客就给大家系统的分析一下多线程的状态, 由于java中线程状态与操作系统中的线程状态划分有区别, 因此这里优先介绍操作系统的 ...

  7. css移动端适配方法

    一:前端开发的常用单位 1.像素(px)     1.什么是像素(Pixel)?     在前端开发中视口的水平方向和垂直方向是由很多小方格组成的, 一个小方格就是一个像素     例如div尺寸是1 ...

  8. JavaScript设计模式样例二十一 —— 解释器模式

    解释器模式(Interpreter Pattern) 定义:提供了评估语言的语法或表达式的方式.目的:对于一些固定文法构建一个解释句子的解释器.场景:编译器.运算表达式计算. // 定义对于语法的断言 ...

  9. **错误积累&&防止GG写法总结

    19.3.20 关于int与ll 1.如果一道题时间充足,把全部变量定义成long long 2.特别注意最上面宏定义的变量类型,特别是FOR 3.如果是int,用1LL* ...... **实例,让 ...

  10. ubuntu 16.04 安装Python3.8虚拟环境

    virtualenv为应用提供了隔离的Python运行环境,可以解决不同应用间多版本的冲突问题. virtualenv会把用户指定版本的python复制到虚拟环境下,并修改相关的环境变量,使得pyth ...