Kotlin 协程:从普通函数到 `suspend` 函数的桥梁

562 阅读3分钟

作为 Kotlin 协程的核心概念之一,suspend 函数为我们提供了强大的异步编程能力。然而,suspend 函数的调用方式与普通函数有所不同,它只能在另一个 suspend 函数内部,或者在协程作用域中被调用。那么,如何在普通函数中调用 suspend 函数呢?本文将详细介绍几种方法,并分析它们的使用场景。

suspend 函数的调用限制

suspend 函数只能在以下两种环境中被调用:

  1. 另一个 suspend 函数内部: 一个 suspend 函数可以调用其他 suspend 函数。
  2. 协程作用域内: 通过 CoroutineScope.launchCoroutineScope.asyncrunBlocking 等方式创建的协程作用域内。

在普通函数中调用 suspend 函数的方法

在普通函数中调用 suspend 函数的方法

由于普通函数不能直接调用 suspend 函数,我们需要借助协程启动器来“桥接”普通函数和 suspend 函数。

  1. runBlocking { ... }

    • 原理: runBlocking 会创建一个新的协程作用域,并阻塞当前线程,直到该作用域内的协程执行完毕。

    • 使用场景:

      • main 函数或者单元测试中调用 suspend 函数。
      • 在非协程作用域中,需要同步等待 suspend 函数执行完成的场景。
      • 用于桥接普通函数和协程,例如初始化操作。
    • 代码示例:

      import kotlinx.coroutines.*
      
      suspend fun fetchData(): String {
          delay(1000)
          return "Data from server"
      }
      
      fun processData(): String {
          return runBlocking {
              val data = fetchData()
              "Processed: $data"
          }
      }
      
      fun main() {
          println(processData())
      }
      
  2. CoroutineScope(Dispatchers.XXX).launch { ... } + Job.join()async { ... }.await()

    • 原理: 使用 CoroutineScope 创建一个协程作用域,并使用 launch 启动一个协程来执行 suspend 函数。然后,使用 Job.join()async().await() 等待协程执行完成。

    • 使用场景:

      • 在普通函数中异步执行 suspend 函数,并且需要等待结果的场景。
      • 在需要控制协程的生命周期时。
      • 在需要使用特定调度器(例如 Dispatchers.IO)时。
    • 代码示例 (使用 launch + join):

      import kotlinx.coroutines.*
      
      suspend fun fetchData(): String {
          delay(1000)
          return "Data from server"
      }
      
      fun processData(): String {
          val scope = CoroutineScope(Dispatchers.Default)
          var result: String = ""
          val job = scope.launch {
              val data = fetchData()
              result = "Processed: $data"
          }
          runBlocking { job.join() }
          scope.cancel()
          return result
      }
      
      fun main() {
          println(processData())
      }
      
    • 代码示例 (使用 async + await):

      import kotlinx.coroutines.*
      
      suspend fun fetchData(): String {
          delay(1000)
          return "Data from server"
      }
      
      fun processData(): String {
          val scope = CoroutineScope(Dispatchers.Default)
          val deferred = scope.async {
              val data = fetchData()
              "Processed: $data"
          }
          val result = runBlocking { deferred.await() }
          scope.cancel()
          return result
      }
      
      fun main() {
          println(processData())
      }
      
  3. withContext(Dispatchers.XXX) { ... }

    • 原理: withContext 是一个 suspend 函数,它允许你在协程中切换 CoroutineDispatcher(协程调度器)。

    • 使用场景:

      • 在需要执行耗时 IO 操作时,切换到 Dispatchers.IO,避免阻塞主线程。
      • 在需要更新 UI 时,切换到 Dispatchers.Main
      • 在需要执行 CPU 密集型计算时,切换到 Dispatchers.Default 或自定义的线程池。
    • 代码示例:

      import kotlinx.coroutines.*
      import kotlin.system.measureTimeMillis
      
      suspend fun fetchData(): String {
          // 模拟网络请求
          delay(1000)
          return "Data from server"
      }
      
      suspend fun processData(): String = withContext(Dispatchers.Default) {
          // 在 Dispatchers.Default 线程池中执行
          println("Processing data on thread: ${Thread.currentThread().name}")
          val data = fetchData()
          "Processed: $data"
      }
      
      fun main() = runBlocking {
          val time = measureTimeMillis {
              println("Before processData on thread: ${Thread.currentThread().name}")
              val result = processData()
              println("After processData on thread: ${Thread.currentThread().name}")
              println(result)
          }
          println("Time taken: $time ms")
      }
      
  4. 将普通函数转换为 suspend 函数

    • 原理: 如果你的普通函数需要频繁调用 suspend 函数,并且可以在协程作用域内使用,那么可以将普通函数本身也声明为 suspend 函数。

    • 使用场景:

      • 当一个函数内部需要多次调用 suspend 函数时。
      • 当一个函数需要在协程作用域内被调用时。
    • 代码示例:

      import kotlinx.coroutines.*
      
      suspend fun fetchData(): String {
          delay(1000)
          return "Data from server"
      }
      
      suspend fun processData(): String {
          val data = fetchData()
          return "Processed: $data"
      }
      
      fun main() = runBlocking{
          println(processData())
      }
      

如何选择合适的方法

  • runBlocking 适用于简单场景,例如 main 函数、测试代码,或者需要同步等待 suspend 函数执行完成的场景。
  • CoroutineScope.launch/async + join/await 适用于需要更灵活地控制协程生命周期、使用特定调度器,或者需要异步执行的场景。
  • withContext(Dispatchers.XXX) { ... }: 适用于需要在协程中切换调度器的场景,例如在 IO 线程和主线程之间切换。
  • 将普通函数转换为 suspend 函数: 适用于函数内部需要多次调用 suspend 函数,并且可以在协程作用域内使用的场景。

总结

在普通函数中调用 suspend 函数需要借助协程启动器,主要有 runBlockingCoroutineScope.launch/asyncwithContext 这几种方式。选择哪种方式取决于你的具体需求和场景。理解这些方法可以帮助你更好地在 Kotlin 中使用协程。