码迷,mamicode.com
首页 > 其他好文 > 详细

kotlin集合——>协程-基础、取消与超时

时间:2021-02-23 14:13:17      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:call   活动   lis   repeat   上下   异步操作   也有   sha   线程   

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集合——>协程-基础、取消与超时

标签:call   活动   lis   repeat   上下   异步操作   也有   sha   线程   

原文地址:https://www.cnblogs.com/developer-wang/p/14429820.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!