Kotlin:在协程中搜索组合快照状态列表时出现 ConcurrentModificationException

sta*_*cks 2 kotlin kotlin-coroutines android-jetpack-compose

我有(我认为的)一个非常简单的概念,我可以在其中刷新待办事项列表的详细信息。我发现,如果有足够的 TODO 项目(几千个)并且按下刷新按钮(因此fetchFreshTodoItemDetails重复调用),那么我会因以下异常而崩溃:

java.util.ConcurrentModificationException
at androidx.compose.runtime.snapshots.StateListIterator.validateModification(SnapshotStateList.kt:278)
at androidx.compose.runtime.snapshots.StateListIterator.next(SnapshotStateList.kt:257)
at com.rollertoaster.app.ui.screens.todo.TodoScreenViewModel$fetchFreshTodoItemDetails$1$1$1.invokeSuspend(TodoScreenViewModel.kt:332)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@97be452, Dispatchers.Main.immediate]

Run Code Online (Sandbox Code Playgroud)

我的视图模型:

java.util.ConcurrentModificationException
at androidx.compose.runtime.snapshots.StateListIterator.validateModification(SnapshotStateList.kt:278)
at androidx.compose.runtime.snapshots.StateListIterator.next(SnapshotStateList.kt:257)
at com.rollertoaster.app.ui.screens.todo.TodoScreenViewModel$fetchFreshTodoItemDetails$1$1$1.invokeSuspend(TodoScreenViewModel.kt:332)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@97be452, Dispatchers.Main.immediate]

Run Code Online (Sandbox Code Playgroud)

尽管我可以想出几种方法来解决这个问题...我仍然“认为”上述方法应该有效,但事实并非如此,这让我有点疯狂。感谢任何帮助。谢谢

编辑: fullListOfTodos 在我的 appStateHolder 中定义

var fullListOfTodos = mutableStateListOf<TodoModel>()

Hor*_*rea 9

问题

正如您所提到的,在列表中进行搜索indexOfFirst需要很长时间。迭代器的实现禁止其并发修改,这意味着您在迭代搜索时不允许更改列表项。此处发生崩溃是因为列表在循环时被修改,产生了ConcurrentModificationException.

您现在可能认为列表在迭代时不可能被修改,因为您的代码是顺序执行的,因此修改是在搜索之后完成的。fetchFreshTodoItemDetails当您多次拨打电话时,就会出现此问题。它创建另一个协程来执行(可能同时)搜索和修改列表的代码。所以基本上,函数调用会互相竞争,如果运气不好的话会互相中断。

原因对你​​来说并不明显

此时,您可能会认为您实现了一种机制,该机制不允许两个协程使用变量并行运行,fetchJob该变量保存一个作业,该作业在调用函数时被取消,然后启动一个新作业。这里的问题是你的假设是错误的。

当您调用 时fetchJob?.cancel(),您基本上会向当前正在执行的协程发送一个取消信号。由于取消是协作性的,协程必须到达暂停点才能取消,这在indexOfFirst调用中不会发生。由于fetchJob?.cancel()不等待完成JobviewModelScope.launch因此可能会在前一个Job完成之前执行,因此与最后一个调用并行执行。垃圾邮件按钮会导致创建许多协程,并在修改列表项时相互破坏。

解决方案

既然您知道了问题所在,那么您很可能可以在阅读更多文档后提出自己的解决方案(我强烈建议,Kotlin 协程指南非常棒)。

您可能会想到一些快速修复方法(我不推荐):

  • 您可以在长时间运行的搜索中使用yield()coroutineContext.isActive来使其与取消机制配合。在这里,您仍然面临并发执行的风险,但风险较低。
  • 您使用它是fetchJob?.cancelAndJoin()为了真正等待最后一个协程完成,但这只会堆积请求并需要将函数标记为suspend,或者您需要启动两个嵌套协程。

我的推荐受到演员模式的启发。您本质上将有一个Channel可以发送信号的地方,即更新数据的意图。该意图将被无限期运行的协程消耗,并且每当出现新意图时才开始搜索过程,但前提是最后一个操作完成。

class MyViewModel : ViewModel() {
    private val updateChannel = Channel<List<Long>>(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    init {
        viewModelScope.launch(Dispatchers.Default) {
            updateChannel.consumeEach { ids ->
                searchAndReplace(ids)
            }
        }
    }

    fun fetchFreshTodoItemDetails(idsToRefresh: List<Long>) {
        updateChannel.trySend(idsToRefresh)
    }

    private suspend fun searchAndReplace(ids: List<Long>) {
        TODO("The logic you had previously")
    }
}
Run Code Online (Sandbox Code Playgroud)

您当然可以根据您的用例进行调整。希望这对您有用,并让您对当前的问题有一个新的视角。