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,那么它就是一种代码异味。在设计合理的基于协程的项目中,应该尽量少用,或者干脆不用。

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

保护原创,请勿转载!

相关推荐
FunnySaltyFish2 小时前
Kotlin 2.2.20 上新:新contract、跨平台编译稳定、默认Swift导出……
kotlin
2501_915106323 小时前
iOS 使用记录和能耗监控实战,如何查看电池电量消耗、App 使用时长与性能数据(uni-app 开发调试必备指南)
android·ios·小程序·uni-app·cocoa·iphone·webview
雨白3 小时前
深入解析 Android 多点触摸:从原理到实战
android
曾经的三心草4 小时前
Python2-工具安装使用-anaconda-jupyter-PyCharm-Matplotlib
android·java·服务器
Jerry5 小时前
Compose 设置文字样式
android
飞猿_SIR5 小时前
android定制系统完全解除应用安装限制
android
索迪迈科技6 小时前
影视APP源码 SK影视 安卓+苹果双端APP 反编译详细视频教程+源码
android·影视app源码·sk影视
孔丘闻言6 小时前
python调用mysql
android·python·mysql
萧雾宇8 小时前
Android Compose打造仿现实逼真的烟花特效
android·flutter·kotlin