Kotlin 中的同步与异步执行:run、runCatching、runBlocking 与 runInterruptible

84 篇文章 5 订阅

在最近,观看了一段关于 runBlocking 的视频,这是对 runBlocking 行为的一个良好的解释。runBlocking 文档强调了几个关键限制和建议:

  • • 不应该在协程中使用。

  • • 将常规阻塞代码桥接到以挂起方式编写的库

  • • 用于 main 函数

  • • 用于测试。

从 run 和 runCatching 开始。

首先,run 和 runCatching 是同步的,而 runBlocking 和 runInterruptible 是异步的。 run 和 runCatching 是 Kotlin 标准库的一部分,可以在所有支持的平台上使用。 runBlocking 和 runInterruptible 是 Coroutines 的一部分。

最好通过例子来理解。我们需要一个类:

data class Event(
    val id: UUID,
    val value: Int,
    val message: String?,
    var badModifyablePropertyForDemoPurposes = "Some string")

run

run 是一个作用域函数(但可以在没有对象的情况下运行)。这意味着你可以在一个对象上调用它,代码块会直接访问对象的属性和方法,而不需要 this (但你也可以使用 this)。另外, run 可以返回一个结果,这个结果可以在后续的步骤中使用。

val event = Event(
  id = UUID.randomUUID(),
  value = 10,
  message = null)

val isEven = event
  .run {
    value % 2 == 0
  }

println("Is Event.value even? $isEven.")

打印结果:Is Event.value even? true.

run 可以修改原始对象。

val event = Event(
  id = UUID.randomUUID(),
  value = 10,
  message = null,
  badModifyablePropertyForDemoPurposes = "Some string")

event.run {
  badModifyablePropertyForDemoPurposes = "Hello"
}

Event(..., badModifyablePropertyForDemoPurposes=Hello)

那么, run 和 apply 有什么区别呢?好吧,主要的区别在于他们的返回值。 run 是灵活的。它可以返回任何类型,不仅仅是它被调用的对象的类型。另一方面, apply 总是返回对象本身,这对于链式对象配置非常好。

此外,如前所述, run 可以独立于对象运行。这与 apply 形成了对比,后者总是需要一个对象来工作。

val event = Event(
  id = UUID.randomUUID(),
  value = 10,
  message = null)

event.message?.let {
  println("The message is $it")
} ?: run {
  println("The message is null")
}

在这个例子中, run 被用作 event.message 为 null 的情况下的后备。

run 非常方便,特别是当它与其他作用域函数结合使用,以保持一致的代码架构时。为了安全,最好确保在 run 块中的代码不容易抛出异常。然而,对于需要异常处理的情况, runCatching 是更好的选择。

runCatching

这是 run 的一个变体。 runCatching 实际上是一个 try...catch 块,但有一个重要的区别。它将块执行的结果封装到一个 Result 对象中。这种封装不仅使代码更易读,而且还便于安全地检索数据。另一个优点是 runCatching 块执行的结果可以被比较。

data class Event(
    val id: UUID,
    val value: Int,
    val message: String?,
    var badModifyablePropertyForDemoPurposes: String)

val event = Event(
    id = UUID.randomUUID(),
    value = 10,
    message = null,
    badModifyablePropertyForDemoPurposes = "Some string")

val result = event.runCatching {
    value / 0
}.onFailure {
    println("We failed to divide by zero. Throwable: $it")
}.onSuccess {
    println("Devision result is $it")
}

println("Returned value is $result")

打印结果:

18:01:58.722  I  We failed to divide by zero. Throwable: java.lang.ArithmeticException: divide by zero
18:01:58.723  I  Returned value is: Failure(java.lang.ArithmeticException: divide by zero)

所以,正如你所看到的,使用 runCatching 提供了几个优势。块执行的结果可以以可链式的方式被消耗,或者被返回到一个变量并在后面处理,例如,在流中发出。

Result 类提供了许多有用的方法和属性来处理持有的值。更有趣的是,你可以扩展它的方法,为异常处理添加更复杂的逻辑。

异步的 runBlocking 和 runInterruptable

runBlockingrunInterruptible 和同步的 run 和 runCatching 之间的唯一共同点是它们能够执行一个代码块。然而, runBlocking 和 runInterruptible 不仅与它们的同名函数 run 和 runCatching 有显著的区别,而且在功能和用例方面也彼此有显著的区别。

为了演示,我们将使用我在关于 Kotlin flows 的一系列文章中使用的 FlowGenerator 类。

class EventGenerator {
    /**
     * Simulates a stream of data from a backend.
     */
    val coldFlow = flow {
        val id = UUID.randomUUID().toString()
        // Simulate a stream of data from a backend
        generateSequence(0) { it + 1 }.forEach { value ->
            delay(1000)
            println("Emitting $value")
            emit(value)
        }
    }
}

这个类提供了一个无限的冷流的单一实例,有一个挂起点(delay)。这个流是可以挂起和取消的。它遵循协程的规则和控制。

它也代表了一个永不结束的异步流,这实际上是我们应该期待的任何流。它有助于更好地理解异步和并行执行问题。

runBlocking

主要用例:

  1. 1. 当需要在一些测试中阻塞直到协程完成执行的时候。

  2. 2. 为了执行一些顶级的代码(也就是说,不能在协程中运行的代码)。

主要的问题是,为什么只有这些呢?

为什么我在 StackOverflow 上看到的回答中,比如说,需要避免使用这个函数?是的,它阻塞了当前的线程,但我们可以产生自己的线程,这样就不会影响其他的代码。

试试看:

private fun runFlows() {
    thread {
        runCollection()
    }
}

private fun runCollection() {
    runBlocking {
        val eventGenerator = EventGenerator()
        eventGenerator
            .coldFlow
            .take(2)
            .collect {
                println("Collection in runCollections #1: $it")
        }
    }

    CoroutineScope(Dispatchers.Default).launch {
        runBlocking {
            val eventGenerator = EventGenerator()
            eventGenerator.coldFlow.collect {
                println("Collection in runCollections #2: $it")
            }
        }
    }
}

在这个例子中,我故意在协程中调用了 runBlocking。尽管文档建议反对这种做法,但这样做不会在 IDE、构建日志或运行时触发任何警告或错误。

这意味着识别和跟踪此类使用情况完全取决于您(开发人员)。

直接插入 runBlocking 相对容易发现和修复。但是,想象一下这样的场景: runBlocking 隐藏在库或其他模块的函数调用后面并且无法轻易发现。行为保持不变,但调试变成了一场噩梦。

18:24:28.091  I  Emitting 0
18:24:28.096  I  Collection in runCollections #1: 0
18:24:29.099  I  Emitting 1
18:24:29.099  I  Collection in runCollections #1: 1
18:24:30.102  I  Emitting 2
18:24:30.102  I  Collection in runCollections #1: 2
18:24:31.103  I  Emitting 3
18:24:31.103  I  Collection in runCollections #1: 3
18:24:32.105  I  Emitting 4
18:24:32.105  I  Collection in runCollections #1: 4

正如您所看到的,日志中没有“Collection in runCollections #2”。原因是流是无限的,永远不会结束。线程永远保持锁定状态。

在实践中,您可能会进行长时间的网络或数据库操作。在 runBlocking 中运行它会严重影响应用程序性能……或库性能

如果流程是有限的,那么协程中的收集将开始,但在正常的异步代码中,下一个操作不应该等待。这是潜在的性能下降。除非您确实需要在其余异步代码开始之前执行某些操作。正如文档中提到的,它可能是外部库处理。

修改

private fun runFlows() {
    thread(name = "Manual Thread") {
        runCollection()
    }

}

private fun runCollection() {
    val coroutine1 = CoroutineScope(Dispatchers.Default).launch {
        runBlocking {
            val eventGenerator = EventGenerator()
            eventGenerator
                .coldFlow
                .collect {
                    println("Collection in runCollections #1: $it")
                }
        }
    }

    val coroutine2 = CoroutineScope(Dispatchers.Default).launch {
        runBlocking {
            val eventGenerator = EventGenerator()
            eventGenerator.coldFlow.collect {
                println("Collection in runCollections #2: $it")
            }
        }
    }
}

out:

21:33:38.848  I  Emitting 0
21:33:38.851  I  Collection in runCollections #1: 0
21:33:38.867  I  Emitting 0
21:33:38.867  I  Collection in runCollections #2: 0
21:33:39.852  I  Collection in runCollections #1: 1
21:33:39.876  I  Collection in runCollections #2: 1
21:33:40.854  I  Emitting 2
21:33:40.854  I  Collection in runCollections #1: 2
21:33:40.879  I  Emitting 2
21:33:40.879  I  Collection in runCollections #2: 2

从日志来看一切正常,两个协程都在运行。这是因为 CoroutineScope(Dispatchers.Default).launch 为协程选择了一个线程,从而减轻了 runBlocking 锁定线程的负面影响。

这种线程管理缓解了协程阻塞的问题,即使在协程上下文中使用 runBlocking 也能确保更顺畅的执行。

1. runFlows
     +- thread 
        +- Thread[Manual Thread,5,main]
2. runFlows
     +- thread 
        +- runCollections
           +- coroutine1
              +- Thread[DefaultDispatcher-worker-3,5,main]
3. runFlows
     +- thread 
        +- runCollections
           +- coroutine1
              +- Thread[DefaultDispatcher-worker-2,5,main]

一切似乎都正常:应用程序不会崩溃,并且性能适中。然而,这种方法提出了其实用性的问题。这里应用程序生成一个协程,该协程又生成一个线程,然后调用 runBlocking 创建另一个协程,并获得与常规使用协程完全相同的行为。

这种方法与高效和可预测代码的原则相矛盾。它破坏了逻辑流程,并使预测对应用程序性能和行为的长期影响变得困难。如果您在代码中遇到此类模式,最好尽快修复代码。

现在,让我们看一下使用 viewModel 的更现实的场景。

class MainViewModel : ViewModel() {
    fun runFlows() {
        thread(
            name = "Manual Thread",
        ) {
            println("Thread: ${Thread.currentThread()}")
            runCollection()
        }

    }

    private suspend fun collect(action: (Int) -> Unit) {
        runBlocking {
            val eventGenerator = EventGenerator()
            eventGenerator
                .coldFlow
                .collect {
                    action(it)
                }
        }
    }

    private fun runCollection() {
        viewModelScope.launch {
            collect {
                println("Collection in runCollections #1: $it: ${Thread.currentThread()}")
            }
        }

        viewModelScope.launch {
            collect {
                println("Collection in runCollections #2: $it: ${Thread.currentThread()}")
            }
        }
    }
}
00:40:44.332  I  Emitting 0
00:40:44.334  I  Collection in runCollections #1: 0: Thread[main,5,main]
00:40:45.336  I  Emitting 1
00:40:45.336  I  Collection in runCollections #1: 1: Thread[main,5,main]
00:40:46.337  I  Emitting 2
00:40:46.338  I  Collection in runCollections #1: 2: Thread[main,5,main]

请注意,生成线程不会提供任何内容,它只是生成一个根本不影响异步操作的线程。 viewModelScope 绑定到主调度程序,最终进入主线程(当然,这是一个简化的解释,因为深入研究了调度程序的细节以及 Main 之间的区别、Main.immediate 不在本文中)。

如果 runBlocking 从 collect() 实现中删除,则调用 runFlows() 打印

01:05:48.180  I  Emitting 0
01:05:48.181  I  Collection in runCollections #1: 0: Thread[main,5,main]
01:05:48.181  I  Emitting 0
01:05:48.181  I  Collection in runCollections #2: 0: Thread[main,5,main]
01:05:49.182  I  Emitting 1
01:05:49.182  I  Collection in runCollections #1: 1: Thread[main,5,main]
01:05:49.183  I  Emitting 1
01:05:49.183  I  Collection in runCollections #2: 1: Thread[main,5,main]

这就是我们通常期望的异步操作。是的,这是预料之中的,但如果您不牢记 viewModelScope 的绑定内容,则并不明显。

将 thread 移动到 collect() 函数

private suspend fun collect(action: (Int) -> Unit) {
        thread(
            name = "Manual Thread",
        ) {
            runBlocking {
                val eventGenerator = EventGenerator()
                eventGenerator
                    .coldFlow
                    .collect {
                        action(it)
                    }
            }
        }
    }

也给出了类似的结果

01:08:51.944  I  Emitting 0
01:08:51.944  I  Emitting 0
01:08:51.946  I  Collection in runCollections #2: 0: Thread[Manual Thread,5,main]
01:08:51.947  I  Collection in runCollections #1: 0: Thread[Manual Thread,5,main]
01:08:52.948  I  Emitting 1
01:08:52.948  I  Emitting 1
01:08:52.948  I  Collection in runCollections #1: 1: Thread[Manual Thread,5,main]
01:08:52.948  I  Collection in runCollections #2: 1: Thread[Manual Thread,5,main]

当然,你应该清楚地了解这种结构发生了什么。使用 runBlocking 您很容易失去对异步操作的跟踪,并失去用于自动管理挂起和切换要执行的协程的强大协程功能。如果您不是 Java 和 Android 线程方面的专家,并且由于某种原因协程实现不符合您的需求,那么这不是最好的。

在其他情况下,将 runBlocked 的使用限制为文档定义的范围。感觉至少在移动应用程序开发中它应该主要用于测试。

runInterruptible

文档指出将以可中断的方式调用代码块。此函数不会生成线程并遵循您作为参数提供的调度程序。

在 viewModel 中添加了新方法。

fun runInterruptible() {
    viewModelScope.launch {
        println("Start")

        kotlin.runCatching {
            withTimeout(100) {
                runInterruptible(Dispatchers.IO) {
                    interruptibleBlockingCall()
                }
            }
        }.onFailure {
            println("Caught exception: $it")
        }
        println("End")
    }
}

private fun interruptibleBlockingCall() {
    Thread.sleep(3000)
}
11:06:29.259  I  Start
11:06:30.431  I  Caught exception: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 100 ms
11:06:30.431  I  End

注意调用链。 runCatching (try…catch )然后是 withTimeout 。这里使用的是 Kotlin 1.9.20, withTimeout 抛出异常,但没有看到它的日志。如果我添加 try…catch 或 runCatching 那么可以检测到异常,没有它 - 协程就默默地停止工作了。

没有找到这种行为的原因,并且在跟踪器中没有看到任何报告。因此请记住使用 try…catch 或 withTimeoutOrNull 。

转自:Kotlin 中的同步与异步执行:run、runCatching、runBlocking 与 runInterruptible

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值