Kotlin协程基础

379 阅读6分钟
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协程并不能保持该进程的生命。 它们就像守护线程一样。