【问题标题】:Why Do You Need to Change Dispatchers in Coroutines?为什么需要更改协程中的调度程序?
【发布时间】:2020-05-23 19:40:38
【问题描述】:

我一直在通过this codelab 了解协程。我仍然不清楚的一件事是,为什么我们需要更改调度程序以确保我们不会阻塞主/UI 线程?如果协程是轻量级线程,那么当我已经在主线程上时,为什么不能在协程中调用线程阻塞函数(无论它们是否挂起)?

codelab 解释说(概括地说)如果我编写这段代码:

// Repository.kt
suspend fun repoRefreshTitle() {
    delay(500)
}

//ViewModel.kt
fun vmRefreshTitle() {
   viewModelScope.launch {
       _spinner.value = true
       repository.repoRefreshTitle()
   }
}

...那么这不会阻塞主线程。 delay() 是一个suspend 函数,所以viewmodelScope.launch 创建的协程会暂停,直到500ms 过去。主线程不会被阻塞。

但是,如果我将 repoRefreshTitle() 重构为以下内容:

suspend fun repoRefreshTitle() {
    val result = nonSuspendingNetworkCall()
}

...那么该网络调用实际上将在主线程上完成。那是对的吗?我将不得不更改为另一个调度程序以将工作卸载到 IO 线程:

suspend fun repoRefreshTitle() {
    withContext(Dispatchers.IO) {
        val result = nonSuspendingNetworkCall()
    }
}

我一定以某种方式过度简化了这一点。我已经在协程中的事实还不够吗?为什么我必须切换调度程序?

【问题讨论】:

  • “但是,如果我将 refreshTitle() 重构为以下内容”——在您的第一个代码 sn-p 中,您有两个名为 refreshTitle() 的函数。为了清楚起见,至少在问题中,如果它们有两个不同的名称会很有帮助。在这种特定情况下,我们不知道您重写了哪一个。我猜它是存储库之一。
  • 非常好,感谢您指出这一点。我做了一些调整,以更清楚地区分这 2 个功能。

标签: android kotlin-coroutines


【解决方案1】:

codelab 解释说(总而言之)如果我编写这段代码......那么这不会阻塞主线程。 delay() 是一个挂起函数,所以 viewmodelScope.launch 创建的协程会被暂停,直到 500ms 过去。主线程不会被阻塞。

正确。然而,delay() 中真正的“工作”将在主应用程序线程上执行,因为viewModelScope.launch() 的默认调度程序基于Dispatchers.Main

但是,如果我将 repoRefreshTitle() 重构为以下内容……那么该网络调用实际上将在主线程上完成。对吗?

正确。 nonSuspendingNetworkCall()delay() 一样,将在主应用程序线程上运行。在nonSuspendingNetworkCall(),这可不是什么好事。

我将不得不更改为另一个调度程序以将工作卸载到 IO 线程

正确。更具体地说,您需要使用使用后台线程的调度程序。对于 I/O,Dispatchers.IO 是常见的选择。

我已经在协程中还不够吗?为什么我必须切换调度程序?

因为我们不想在主应用程序线程上进行网络 I/O。 Dispatchers.Main 在主应用程序线程上运行其协程,这是 viewModelScope.launch() 的默认调度程序。这就是为什么在我写的很多东西中,我都特别写 viewModelScope.launch(Dispatchers.Main) 的原因之一——这更罗嗦(在技术上与默认值略有不同),但对读者来说更明显。

【讨论】:

  • 我明白了,所以在viewmodelScope 中创建的协程,它仍然在主线程上运行。它不像是与主线程分离的轻量级线程。对吗?
  • @coolDude:正确。如果你曾经在HandlerView 上使用过post(),那么在Dispatchers.Main 上运行协程类似于post()-ing RunnableRunnable 仍然在主应用程序线程上运行,只是稍后。
  • 我明白了。感谢您的澄清,非常感谢!
【解决方案2】:

当您在viewModelScope 中运行代码时,这并不意味着您的主线程不会冻结。它只是确保如果您开始在 MainThread 上工作并且您正在等待另一个线程返回结果,它不会阻塞主线程,例如使用 Retrofit 调用 API 并等待更新 ViewModel 中的 LiveData。

那么为什么需要改变 Coroutine Scope? (可能使用withContext

您在主线程上开始工作并切换到另一个协程进行繁重的工作,并在结果准备好后轻松地将结果返回到主线程。

fun onSaveImageFile(source: Int, filename: String) = viewModelScope.launch {
    val isFileSaved = withContext(Dispatchers.IO) {
        FileRepository.saveImageFile(source, filename)
    }
    toastViewModel.postValue(if (isFileSaved) "Image file saved!" else "Failed to save image file!")
}

【讨论】:

  • 我明白了,现在完全有道理了。我的印象是,既然人们说协程就像轻量级线程,它们就会像与主线程分离一样运行。感谢您的澄清!
  • @coolDude 欢迎您。阅读 Coroutines 中的 Main Safty,它可以清除所有内容。如果你还没有,也可以考虑使用 RxJava。
  • 使用 Retrofit 是 IO 的东西,所以也应该使用 Dispatchers.IO 调用
【解决方案3】:

看到这个documents,它描述了Dispatchers.IO是专门为I/O操作而设计的

viewModelScope.launch {

}

创建一个可感知lifecycle 的协程块,并适用于任何非专用于I/O 操作的异步操作。当您的ViewModel 被销毁时,

viewModelScope.launch{
    // Invoke network suspend functions from repository
    // Or any kind of asynchronous operation
}

将被停止并取消,这将取消此块

withContext(Dispatchers.IO) {
    // Invoke only I/O operations
}

也是因为viewModelScopewithContext(Dispatchers.IO) 保持通信。

您不应该让您的 viewModelScope 忙于 I/O 操作,而应该让另一个 I/O 专用协程线程维护该 I/O 操作并跟踪 viewModelScope。这将使viewModelScope 的重量更轻。

【讨论】:

    猜你喜欢
    • 2017-01-28
    • 2021-02-25
    • 1970-01-01
    • 2022-01-04
    • 2017-04-16
    • 1970-01-01
    • 2011-08-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多