runBlocking实践:哪里该使用,哪里不该用

本文译自「runBlocking in practice: Where it should be used and where not」,原文链接kt.academy/article/run...,由Marcin Moskała发布于2025年9月1日。

传统意义上讲,Java 和 Kotlin 项目都基于阻塞调用(blocking)。我所说的阻塞调用是指函数在等待某些操作(例如,等待网络响应)时会阻塞调用者的线程。Kotlin 协程最重要的规则之一是,我们不应该在挂起函数(suspending function)上进行阻塞调用(除非我们使用允许阻塞调用的调度程序,例如 Dispatchers.IO)。

kotlin 复制代码
// Incorrect: blocking call in a suspending function
suspend fun getUser(): User {
    val response = api.getUser() // blocking call
    return response.toDomainUser()
}

// Correct: Using withContext(Dispatchers.IO) to make a blocking call in a suspending function
suspend fun getUser(): User = withContext(Dispatchers.IO) {
    val response = api.getUser() // blocking call
    response.toDomainUser()
}

但是如何反其道而行之呢?如何将挂起调用转换为阻塞调用?为此,我们使用 runBlocking

[runBlocking 的工作原理](#runBlocking 的工作原理 "#how-runblocking-works")

runBlocking 在调用它的线程上启动一个协程,并阻塞该线程直到协程完成。因此,runBlocking 本质上是同步的,因为如果我们多次调用它,第二个调用要等到第一个调用完成后才会启动。作为一个同步协程构建器,runBlocking 返回它启动的协程的结果。

kotlin 复制代码
fun main() {
    log("Starting main")
    runBlocking {
        log("Starting first runBlocking")
        delay(1000)
        log("Finishing first runBlocking")
    }
    val result: String = runBlocking {
        log("Starting second runBlocking")
        delay(1000)
        "ABCD"
    }
    log("Second runBlocking finished with result: $result")
}

fun log(message: String) {
    println("[${Thread.currentThread().name}] $message")
}
// [main] Starting main
// [main] Starting first runBlocking
// (1 sec)
// [main] Finishing first runBlocking
// [main] Starting second runBlocking
// (1 sec)
// [main] Second runBlocking finished with result: ABCD

由于 runBlocking 启动了一个作用域,它会等待其中启动的所有协程完成。这意味着它会等待所有子协程完成。这就是为什么下面的程序要等到所有三个异步协程都完成才会完成。为了展示如何使用 runBlocking 定义结果,我还让这个程序从 main 函数返回 0

kotlin 复制代码
import kotlinx.coroutines.*

fun main(): Int = runBlocking {
    launch { delayAndPrintHello() }
    launch { delayAndPrintHello() }
    launch { delayAndPrintHello() }
    println("Hello")
    0 // result from main
}

suspend fun delayAndPrintHello() {
    delay(1000L)
    println("World!")
}
// Hello
// (1 sec)
// World!
// World!
// World!

runBlocking 的行为可能会 让你想起 coroutineScope,这并非巧合,因为它们都启动同步协程,但 runBlocking 是阻塞的,而 coroutineScope 是暂停的。这意味着完全不同的用法,我们只在暂停函数中使用 coroutineScope,而我们永远不应该在暂停函数中使用 runBlocking。这也意味着 coroutineScope 与其调用者建立关系,并且始终处于协程层次结构的中间,而 runBlocking 则启动一个新的协程层次结构。

[使用 runBlocking 的实践](#使用 runBlocking 的实践 "#the-practice-of-using-runblocking")

在正确实现的基于协程的项目中,并使用设计良好的协程友好库,我们几乎不需要使用 runBlocking。如果我们在项目中经常使用它,那就被认为是代码异味。然而,在某些情况下,runBlocking 是有用的,甚至是必要的。也有一些情况下,runBlocking 不应该被使用。我们还会讨论那些曾经需要 runBlocking 但现在有了更好的替代方案的情况。现在,让我们来看看。

[在哪里使用 runBlocking](#在哪里使用 runBlocking "#where-to-use-runblocking")

runBlocking 应该用于需要启动协程并阻塞当前线程直到其完成的情况。这意味着它可以在以下情况下使用:

  • 我们需要等待协程的结果。
  • 我们可以阻塞当前线程。

一个常见的 Android 示例是在 Retrofit 客户端中设置一个拦截器,将令牌附加到网络调用。获取令牌可能需要发起网络调用,因此我们需要启动一个协程来获取令牌。同时,拦截器需要结果才能继续执行。这个拦截器在 Retrofit 的池中启动,因此可以调用它的调用。这使得它成为使用 runBlocking 的理想场所。

kotlin 复制代码
class AddTokenInterceptor: Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = runBlocking { getToken() }
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer $token")
            .build()
        return chain.proceed(request)
    }
}

在后端系统上,有时我们需要阻塞当前线程以等待协程完成,例如为了让我们的工具正确测量此进程的执行时间,或者当我们需要调用某些阻塞脚本并获取其结果时。

kotlin 复制代码
@MeasureExecutionTime
fun runDataMigrationScript() = runBlocking {
    val sourceData = readDataFromSource()
    val transformedData = transformData(sourceData)
    writeDataToTarget(transformedData)
}

这些情况很少见,大多数后端项目不需要使用 runBlocking。除非它们有一些基于阻塞调用的遗留代码。考虑以下 UserService,它在我们的应用程序中用于管理用户。我们已经将其迁移到暂停调用,但我们仍然有一些基于阻塞调用的遗留控制器和服务。为了避免重写所有这些,我们可以为暂停函数提供阻塞替代方案。这些替代方案可以通过使用 runBlocking 包装暂停函数来实现(你也可以考虑使用一些调度器)。

kotlin 复制代码
class UserService(
    private val userRepository: UserRepository,
) {
    suspend fun findUserById(id: String): User = userRepository.findUserById(id)

    // Blocking alternative for legacy parts of our application
    fun findUserByIdBlocking(id: String): User = runBlocking {
        findUserById(id)
    }
    
    // ...
}

这可能是 runBlocking 最重要的用途,它充当了从阻塞到暂停的桥梁。一些库为 Java 定义了阻塞替代方案。

kotlin 复制代码
suspend fun readDataFromSource(): Data {
    // ...
}

fun readDataFromSourceBlocking(): Data = runBlocking {
    readDataFromSource()
}

[哪些情况下不应使用 runBlocking](#哪些情况下不应使用 runBlocking "#where-not-to-use-runblocking")

在某些情况下,不应使用 runBlocking。此外,切勿在挂起函数中直接使用 runBlockingrunBlocking 会阻塞当前线程,因此不应在挂起函数中进行阻塞调用(除非使用允许阻塞调用的调度程序,例如 Dispatchers.IO)。在这种情况下,很可能不需要 runBlocking

kotlin 复制代码
// Incorrect: runBlocking in a suspending function
suspend fun getToken() = runBlocking {
    // ...
}

// runBlocking is most likely not needed
suspend fun getToken() {
    // ...
}

不应在不需要等待结果的函数中使用 runBlocking。如果你只需要启动协程,通常最好使用 launch 启动异步协程。

kotlin 复制代码
// Incorrect: runBlocking used where we do not need to await result
fun startBackgroundProcess() = runBlocking {
    doSomething()
}

// Correct: Using launch to start an asynchronous coroutine
fun startBackgroundProcess() {
    backgroundScope.launch {
        doSomething()
    }
}

我们还应注意,不要在不应被阻塞的线程上使用 runBlocking。这在 Android 上尤其成问题,因为阻塞主线程会导致应用程序卡死。

kotlin 复制代码
// Incorrect: runBlocking on the main thread
fun onClick() = runBlocking {
    userNameView.test = getUserName()
}

// Correct: Using launch to start an asynchronous coroutine
fun onClick() {
    lifecycleScope.launch {
        userNameView.test = getUserName()
    }
}

在后端,如果我们在 synchronized 块中使用它,可能会出现问题。一个技巧是使用 launch 实现回调函数。但是,通常情况下,最好重新设计代码,使用暂停而不是阻塞调用,并使用协程友好的工具(我们将在_同步协程_课程中讨论)。

kotlin 复制代码
// Possibly incorrect: runBlocking inside synchronized block
synchronized(lock) {
    // ...
    val user = runBlocking { getUser() }
    // ...
}

// One solution: use launch to implement a callback
fun getUser(callback: (User) -> Unit) {
    backgroundScope.launch {
        val user = getUser() // suspending call
        callback(user)
    }
}
synchronized(lock) {
    // ...
    getUser { user ->
        // ...
    }
}

[过时的 runBlocking 用法](#过时的 runBlocking 用法 "#outdated-runblocking-uses")

runBlocking 传统上用于包装 main 函数体。它的属性非常适合此目的:它启动一个协程,因此它可以调用挂起函数或启动其他协程,并且它会阻塞线程直到协程完成,因此我们可以确保程序不会在所有这些进程完成之前结束。

kotlin 复制代码
fun main(): Unit = runBlocking {
    val user = getUser() // suspending call
    println("User: $user")
}

runBlocking 仍然可以以这种方式使用,但是在大多数现代情况下,我们更喜欢使用 Kotlin 1.3 中引入的挂起 main 函数。此类函数在底层被一个类似于 runBlocking 的阻塞构建器包装。

kotlin 复制代码
suspend fun main() {
    val user = getUser() // suspending call
    println("User: $user")
}

关键区别在于 runBlocking 设置了一个调度器,使其所有子协程在其正在使用的同一线程上运行。挂起 main 函数不会设置调度器,因此其子协程默认在不同的线程上运行。引入此更改是因为 runBlocking 使用的单线程调度器经常导致意外行为。

kotlin 复制代码
fun main(): Unit = runBlocking {
    println(Thread.currentThread().name)
    launch {
        println(Thread.currentThread().name)
    }
}
// main
// main
kotlin 复制代码
suspend fun main(): Unit = coroutineScope {
    println(Thread.currentThread().name)
    launch {
        println(Thread.currentThread().name)
    }
}
// main
// DefaultDispatcher-worker-1

runBlocking 的第二个传统用途是在测试中。它被用来包装测试主体,以便我们可以调用挂起函数并在其中启动协程。现在,我们更倾向于使用 kotlinx-coroutines-test 库中的 runTest,它是 runBlocking 的一个更强大、更灵活的替代方案。它允许我们控制时间、生成后台作用域并跟踪子协程上的异常。runTest 将在_测试协程_课程中讨论。

kotlin 复制代码
class UserRepositoryTest {
    val userRepository = InMemoryUserRepository()
    val userService = UserService(userRepository)
    
    @Test
    fun testGetUser() = runTest { // previously runBlocking
        // given
        userRepository.hasUser(UserEntity("1234", "John Doe"))
        
        // when
        val user = userService.getUser("1234")
        
        // then
        assertEquals("John Doe", user.name)
    }
}

总结

  • runBlocking 是一个阻塞协程构建器,它启动一个协程并阻塞当前线程直到它完成。
  • runBlocking 是从阻塞世界(blocking)到挂起世界(suspending)的桥梁,它用于在需要阻塞当前线程直到协程完成的地方启动协程。
  • 如果你需要在项目中频繁使用 runBlocking,那么它就是一种代码异味。在设计合理的基于协程的项目中,应该尽量少用,或者干脆不用。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
阿巴斯甜9 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker10 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952711 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android