文档翻译-Kotlin协程 基本用法
该文档由作者自己的理解翻译,若有出入,敬请谅解。
Kotlin协程的基本用法
这一篇我们学习协同程序的基本概念。查看原文
你的第一个协同程序
复制下面的代码到你的程序中并运行:
1 | import kotlinx.coroutines.* |
你将看到如下结果:
1 | Hello, |
本质上,协同程序是轻量级线程。它们被启动在一些协同程序范围的上下文。现在我们启动一个新的协同程序在全局范围,这意味着这个新的协同程序的生命周期被整个应用的生命周期所限制。
替换成 GlobalScope.launch { ... }
与 thread { ... }
和delay(...)
与 Thread.sleep(...)
, 你将能够得到同样的结果。试试吧(别忘记引入kotlin.concurrent.thread
)。
如果你用thread替换GlobalScope.launch,编译器将出现以下错误:
1 | Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function |
那是因为delay是一个特殊的挂起方法不会阻塞线程,但是挂起协同程序和它都只能在协同程序中使用。
桥接阻塞和非阻塞世界
第一个例子将非阻塞的delay{} 和阻塞的Thread.sleep()混合在同一段代码中。这将很容易的忘记哪个是阻塞的哪个是非阻塞的。让我们使用runBlocking协程创建者明确关于阻塞:
1 | import kotlinx.coroutines.* |
结果是一样的,但是这次的代码仅仅使用的是非阻塞的delay。在主线程唤起 runBlocking 阻塞直到runBlocking内的协程执行完成。
这个举例也能够使用更加通用的方式来重写,使用runBlocking去包裹主方法的执行:
1 | import kotlinx.coroutines.* |
这里的runBlockingUnit
,因为一个好的主方法结构必须要返回Unit
。
这也是写挂起方法的单元测试的一种方式。
1 | class MyTest { |
等待任务
当其他协程正在执行时,我们等待一段时间并不是一个好的方式。让我们明确的等待直到我们开启的后台工作完成。
1 | import kotlinx.coroutines.* |
查看完整代码
现在的结果仍然是一样的,但是主协程的代码不依赖于任何后台工作的时长。更加好。
结构并发
我们仍然有一些期望更加实用的协程。当我们使用GlobalScope.launch
,我们创建 一个顶级的协程。尽管这是轻量级的,它运行时仍然会消耗一些内存资源。如果我们忘了给它指向新的引用,那么它会一直运行。如果代码在协程中挂起(例如,我们错误的延时很长时间),如果我们开启了太多的协程导致超过内存限制会怎么样?不得不手动的保持所有开启的协程引用,链接他们是错误的想发(倾向)。
有一个更好的解决办法。我们可以使用并发结构的代码。为了如我们使用线程(线程都是全局的)一样的在全局启动协程,我们可以在一个我们能够控制的一个域中启动协程。
回到我们的例子,我们由main
方法已经被转变成使用runBlocking创建的协程。所有的协程创建方式,包括runBlocking
,在代码块中添加了一个协程域的实例。我们能够在这个域中开启一个协程并且明确的没有join
到域中,因为外层协程不会执行完成知道它的域开启的所有协程都完成。所以,我也可以类似的改造我们的例子:
1 | import kotlinx.coroutines.* |
域的创建
除了不同创建者提供的协程域,你也可以使用coroutineScope
创建声明自己的域。它创建的一个协程域不会完成直到所有开启的子协程完成。runBlocking
和coroutineScope
可能看起来比较相似,因为他们都是等待他们内部和所有子协程完成。这两个的主要不同是,runBlocking
方法阻塞了当前线程去等待,而coroutineScope
仅仅是挂起,释放下面的线程给其他用。由于上述的不同,runBlocking
是一个普通的方法而coroutineScope
是一个挂起的方法。
下面的例子演示一下:
1 | import kotlinx.coroutines.* |
查看所有代码
从结果注意到当延时任务执行等待时,”Task from coroutine scope”就已经执行了, “Task from runBlocking”也会执行和打印,尽管coroutineScope
还没有执行完成。
提取方法重构
让我们提取代码块中的launch{}
成一个独立的方法。当你把代码提取出来成一个新的方法,需要加上suspend
修饰。这就是你的第一个挂起的方法。挂起方法和普通方法一样能够在协程中被使用,但是额外的特点是他们能够有序、使用其他挂起的方法,就像这个例子中的delay{}
,在协程中挂起执行。
1 | import kotlinx.coroutines.* |
查看完整代码
但是如果提取的方法中包含协程的被执行的创建者在当前的域中会怎么样?这种情况下仅仅在提取的方法上添加suspend
修饰符是不够的。在CoroutineScope
写一个扩展的方法是一个解决办法,但是它可能不总是合适的由于它没有清理的API。管用的方法是在一个包含目标方法的类中显式的有一个CoroutineScope
域或者外部实现CoroutineScope
的类有隐式的域。作为最后的手段,使用CoroutineScope(coroutineContext),但是这种方法在结构上是不安全的,因为你在这个域中没有了执行方法的控制权。仅私有API能够使用这个创建者。
协程是轻量级
执行下面的代码:
1 | import kotlinx.coroutines.* |
查看完整代码
开启十万个协程,一秒后分别打印一个点。然后尝试使用线程做这件事。会发生什么?(很有可能会产生内存不足的错误)。
全局的协程就像是后台守护线程
下面的代码开启了一个长时间运行的协程在全局域,它每一秒打印I'm sleeping
然后等待一些时间后从主方法中退出。
1 | import kotlinx.coroutines.* |
查看完整代码
你运行后可以看到以下三行打印然后结束:
1 | I'm sleeping 0 ... |
我们在全局域启动的活跃协程不会保持进程的存活。他们就像是守护线程。