【问题标题】:Organizing Kotlin's Coroutines组织 Kotlin 的协程
【发布时间】:2021-09-30 16:45:24
【问题描述】:

我经常问自己如何使用协程。每次单击按钮或出现其他事件时,我都会启动一个协程来从/向数据库或 rest api 保存或加载数据。然后我有像下面这样的小功能。

在极少数情况下,如果两个或多个协程同时写入/读取,我会得到 ConcurrentModificationExceptions。我从来没有遇到过 Java+RxJava 的这个问题。现在我只使用 Kotlin+Coroutines(没有 RxKotlin,没有 Flow,没有 LiveData)。作为数据库,我使用 Room。

有没有办法保存对 Coroutine-Container 之类的引用,我可以在其中添加 Jobs 以使它们一个接一个地完成?或者你们实际上是如何启动协程的?

fun loadAllDataForTheUserInterface() {
    viewModelScope.launch {
        val newData = dataBaseRepository.load(...)
        fragment.draw(newData)
    }
}

fun handleSaveClick(user: User) {
    viewModelScope.launch {
        val newUser = restApiRepository.uploadNewUser(user)
        databaseRepository.save(newUser)
        fragment.close()
    }
}

我的堆栈跟踪

java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.next(ArrayList.java:860)
    at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:96)
    at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:61)
    at com.google.gson.Gson.toJson(Gson.java:704)
    at com.google.gson.Gson.toJson(Gson.java:683)
    at com.google.gson.Gson.toJson(Gson.java:638)
    at com.google.gson.Gson.toJson(Gson.java:618)
    at my.supercool.app.component.module.JsonModule.toJson(JsonModule.kt:12)
    at my.supercool.app.data.database.MyRoomObjectConverter.fromObjectToJson(MyRoomObjectConverter.kt:29)
    at my.supercool.app.data.database.MyRoomObjectConverter.fromSomeListToJson(MyRoomObjectConverter.kt:71)
    at my.supercool.app.data.database.dao.SomeDao_Impl$1.bind(SomeDao_Impl.java:101)
    at my.supercool.app.data.database.dao.SomeDao_Impl$1.bind(SomeDao_Impl.java:47)
    at androidx.room.EntityInsertionAdapter.insertAndReturnId(EntityInsertionAdapter.java:113)
    at my.supercool.app.data.database.dao.SomeDao_Impl$4.call(SomeDao_Impl.java:142)
    at my.supercool.app.data.database.dao.SomeDao_Impl$4.call(SomeDao_Impl.java:137)
    at androidx.room.CoroutinesRoom$Companion$execute$2.invokeSuspend(CoroutinesRoom.kt:61)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at androidx.room.TransactionExecutor$1.run(TransactionExecutor.java:47)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:923)

【问题讨论】:

  • 如果您已经知道每次单击按钮时肯定会运行两个作业,那么有一种方法可以根据您的需要同步或异步运行作业:)
  • 协程有 launchasync 块可以做到这一点,如果你还想解释一下,请告诉我:)
  • 如果您的 ViewModel 引用了 Fragment,这是一个主要的禁忌。至于您手头的问题,我认为我们需要查看MyRoomObjectConverter 的代码才能知道出了什么问题。您可能需要同步某些内容或小心使用调度程序。
  • 您应该研究 Flows,因为这可能是您正在寻找的范例。您可以拥有一系列给定的 GUI 事件,并为每个事件执行所需的操作。直到前一个事件已经通过,下一个事件才会通过流,从而消除了并发问题。
  • 看来我的主要问题是我认为启动的协程(SomeScope.launch)会在后台以某种方式进行管理,不会相互竞争或同意。正如我现在了解到的,启动的协程就像启动一个新线程一样。启动多个启动似乎不是线程安全的,我认为是这种情况。有了这些知识,ConcurrentModifactionException 就不足为奇了。

标签: android kotlin architecture kotlin-coroutines


【解决方案1】:

对于您提供的示例,我认为演员模型会很好用。在 Actor 模型中,您有一个协同程序,它在循环中运行以运行一些任务。系统的其他部分在有工作要做时向参与者发送消息。

首先,定义一个演员。 actor 协程构建器函数会启动一个协程来运行我们的任务,同时还会创建一个 Channel,我们可以使用它向它发送任务。

val viewModelTasks = viewModelScope.actor<() -> Unit> {
    for (task in this) {
        task.invoke()
    }
}

当你有任务要执行时,把它发送给演员:

val myTask = { doSomeWork() }
viewModelTasks.send(myTask)

actor 将遍历它收到的任务,在开始下一个之前完成每个任务。

因为 actor 是一个单一的协程,并且一次只会运行一个任务,所以这是保护资源免受不同线程并发访问的好方法。

但是,当您需要获取任务的结果或等待任务完成时,Actor 不太适合。如果您需要这些东西,您可能需要考虑不同的解决方案。

【讨论】:

    【解决方案2】:

    我发现这个问题的实际解决方案是在 withContext-block 中调用代码。这样,您的协同程序将一个接一个地执行,并且不会相互竞争或更糟,就像在我的情况下,编辑相同的资源并导致错误。

    如果我知道它是如何工作的以及如何使用它,我会尽快编辑这个答案。现在这里是一个以https://www.baeldung.com/kotlin/withcontext-vs-async-await开头的链接

    【讨论】:

      猜你喜欢
      • 2018-04-07
      • 2018-05-13
      • 1970-01-01
      • 2019-02-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-19
      相关资源
      最近更新 更多