import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // launch a new coroutine in background and continue
delay(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 delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
本质上,协程是轻量级的线程。 它们在某些CoroutineScope的上下文中与启动协程生成器一起启动。 在这里,我们正在GlobalScope中启动一个新的协程,这意味着新协程的寿命仅受整个应用程序寿命的限制。 通过将GlobalScope.launch {...}替换为Thread{...},并将delay(...)替换为Thread.sleep(...),可以实现相同的结果。 尝试一下(不要忘记导入kotlin.concurrent.thread)。
如果首先用线程替换GlobalScope.launch,则编译器将产生以下错误:
Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function
这是因为delay是一个特殊的挂起函数,它不会阻塞线程,而是挂起协程,并且只能在协程中使用。
Bridging blocking and non-blocking worlds
第一个示例在同一代码中混合了非阻塞delay(...)和阻塞Thread.sleep(...)。 很容易忘记哪个阻塞了,哪个没有阻塞。 让我们明确地使用runBlocking协程生成器进行阻止:
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // launch a new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main thread continues here immediately
runBlocking { // but this expression blocks the main thread
delay(2000L) // ... while we delay for 2 seconds to keep JVM alive
}
}
结果是相同的,但是此代码仅使用非阻塞延迟。 调用runBlocking的主线程将阻塞,直到runBlocking内部的协程完成。
还可以使用runBlocking封装主要函数的执行,以更惯用的方式重写此示例:
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> { // start main coroutine
GlobalScope.launch { // launch a new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
在这里,runBlocking {...}用作用于启动顶级主协程的适配器。 我们明确指定其Unit返回类型,因为Kotlin中格式良好的main函数必须返回Unit。
这也是编写用于挂起功能的单元测试的方法:
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking<Unit> {
// here we can use suspending functions using any assertion style that we like
}
}
Waiting for a job
在另一个协程工作时延迟一段时间不是一个好方法。 让我们显式等待(以非阻塞方式),直到我们启动的后台作业完成为止:
val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes
现在结果仍然相同,但是主协程的代码不以任何方式与后台作业的持续时间绑定。 这样写好多了。
Structured concurrency 结构化并发
协程的实际使用仍需要一些东西。 当我们使用GlobalScope.launch时,我们将创建一个顶级协程。 即使它很轻巧,它在运行时仍会消耗一些内存资源。 如果我们忘记保留对新发布的协程的引用,它仍然会运行。 如果协程中的代码挂起(例如,我们错误地延迟了太长时间),怎么办?如果启动太多协程并用完了内存怎么办? 必须手动保留对所有已启动协程的引用并加入它们是容易出错的。 有更好的解决方案。 我们可以在代码中使用结构化并发。 像我们通常对线程(线程始终是全局的)一样,不像在GlobalScope中启动协程那样,我们可以在执行的操作的特定范围内启动协程。
在我们的示例中,我们有一个主要功能,可以使用runBlocking协程生成器将其转换为协程。 每个协程生成器,包括runBlocking,都会在其代码块范围内添加一个CoroutineScope实例。 我们可以在此范围内启动协程,而不必显式地加入它们,因为外部协程(在我们的示例中为runBlocking)直到在其范围内启动的所有协程完成后才完成。 因此,我们可以使示例更简单:
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine in the scope of runBlocking
delay(1000L)
println("World!")
}
println("Hello,")
}
Scope builder
除了不同构建器提供的协程作用域之外,还可以使用coroutineScope构建器声明自己的作用域。 它创建一个协程范围,直到所有启动的子级都完成才完成。
runBlocking和coroutineScope可能看起来相似,因为它们都等待身体及其所有子对象完成。 主要区别在于,runBlocking方法阻止当前线程等待,而coroutineScope只是挂起,释放基础线程以供其他用途。 由于存在这种差异,因此runBlocking是常规函数,而coroutineScope是挂起函数。
可以通过以下示例进行演示:
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("Task from runBlocking")
}
coroutineScope { // Creates a coroutine scope
launch {
delay(500L)
println("Task from nested launch")
}
delay(100L)
println("Task from coroutine scope") // This line will be printed before the nested launch
}
println("Coroutine scope is over") // This line is not printed until the nested launch completes
}
Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over
提取功能重构
让我们将launch{...}中的代码块提取到一个单独的函数中。 在此代码上执行“提取函数”重构时,您将获得带有suspended修饰符的新函数。 这是您的第一个暂停功能。 暂停函数可以像常规函数一样在协程内部使用,但是它们的附加功能是它们可以依次使用其他暂停函数(例如本示例中的延迟)来暂停协程的执行。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch { doWorld() }
println("Hello,")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
但是,如果提取的函数包含在当前范围内调用的协程生成器,该怎么办? 在这种情况下,提取的函数上的suspend修饰符是不够的。 解决方案之一就是使doWorld成为CoroutineScope的扩展方法,但由于它无法使API更清晰,因此它可能并不总是适用。 惯用的解决方案是在包含目标函数的类中具有显式的CoroutineScope作为字段,或者在外部类实现CoroutineScope时具有隐式的。 作为最后的手段,可以使用CoroutineScope(coroutineContext),但是这种方法在结构上是不安全的,因为您不再可以控制此方法的执行范围。 仅专用API可以使用此构建器。
Coroutines ARE light-weight
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
启动10万个协程,并在5秒钟后每个协程打印一个点。
现在,尝试使用线程。 会发生什么? (您的代码很可能会产生某种内存不足错误)
全局协程就像守护线程
下面的代码在GlobalScope中启动一个长时间运行的协程,该协程每秒打印两次“I'm sleeping”,然后在延迟一段时间后从主函数返回:
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // just quit after delay
可以运行并看到它显示三行并终止:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
在GlobalScope中启动的active协程并不能保持该进程的生命。 它们就像守护线程一样。