在最近,观看了一段关于 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
runBlocking
、runInterruptible
和同步的 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. 当需要在一些测试中阻塞直到协程完成执行的时候。
-
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