标签: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
标签:call 活动 lis repeat 上下 异步操作 也有 sha 线程
原文地址:https://www.cnblogs.com/developer-wang/p/14429820.html