我的代码很简单,但会导致奇怪的IndexOutOfBoundsResponse.

It happens only once in ten thousands,但它发生了.我只能在Firebase Crashlytics报告中看到它们.

你能看到这个问题吗?还是我应该忽略它?

private val mTaskList = ArrayList<Task>()
// a element is added to the list.
// The list usually contains only one element.
private var w或ker: TaskW或ker? = null
...
// Only one TaskW或ker can exist.
  if(w或ker==null){
    Thread(TaskW或ker().also{
      w或ker = it
    }).start()
  }
...

inner class TaskW或ker : Runnable {
  override fun run() {
    while (mTaskList.size > 0) {
      TaskList.removeAt(0).run {  // causes java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 (或 Index: 0, Size: 0)
        ...
      }
    }
    w或ker = null // Only one TaskW或ker can exist.
  }
}

TaskList.removeAt(0)导致执行. (I can see them only in the Firebase Crashlytics rep或t)

Fatal Exception: java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1

Fatal Exception: java.lang.ArrayIndexOutOfBoundsException: Index: 0, Size: 0

StackTrace就是这样的

java.util.ArrayList.remove (ArrayList.java:503)
<my package>.MainActivity$TaskW或ker.run (MainActivity.kt:452)
java.lang.Thread.run (Thread.java:923)

推荐答案

IndexOutOfBoundsException的原因很可能是竞争条件,这意味着多个线程竞争单个资源,即对mTaskList的访问.

当两个线程同时运行时,它们都可以判断mTaskList.size > 0是否为真,为什么它们然后both继续执行TaskList.removeAt(0)(我假设实际上是mTaskList.removeAt(0)).第一个成功,第二个引发异常,因为没有剩余的元素需要删除.

假设除了TaskWorker之外,没有对该数组的其他写访问,那么罪魁祸首很可能是工作线程的初始化.有四种相关的非原子操作.non-atomic意味着另一个线程可能会干扰,因为它们不是作为不可分割的单元执行的:

  1. if (worker == null)
  2. TaskWorker()
  3. worker = it
  4. Thread::start()

这是一个比数组更糟糕的竞争条件:两个线程都可以判断(1.)然后错误地认为他们可以安全地执行其余的操作.将有两个TaskWorker个实例(2.),但只有第二个变量可由变量worker(3.)访问.不过,第一个仍然存在于前Thread对象中,并且两个Thread都将与它们自己的TaskWorker实例同时被start化(4.)这反过来又是导致比赛条件为mTaskList的原因.

有多种方法可以解决这个问题.正如@broot在 comments 中建议的那样,您可以确保对TaskWorker初始化的多次调用以及对数组的访问仅在同一线程上执行.这样就可以保证没有其他线程可以干扰.实现这一点的最简单方法是将调用留在主线程上.然而,如果负载太重,这可能不是一个 Select .在这种情况下,您可以通过定义具有newSingleThreadContext的新调度程序来将调用限制在特定线程上.查看https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html#thread-confinement-coarse-grained了解更多信息.

另一种方法是依赖Kotlin协同程序的 struct 化并发性,并首先避免创建自己的线程.将您的TaskWorker类替换为:

suspend fun executeTasks() = withContext(Dispatchers.Default) {
    while (true) {
        ensureActive()
        val task = taskListMutex.withLock { mTaskList.removeFirstOrNull() } ?: break
        // do whatever you need to do with the task
    }
}

为了实现这一目标,您还需要一个Mutex实例,该实例可能最好位于mTaskList处:

private val taskListMutex = Mutex()

现在,您只需调用executeTasks(),而不是创建Thread来开始任务执行.我 Select Dispatchers.Default是因为我假设任务的计算量很大.如果与IO相关,则应该使用Dispatchers.IO.或者您将所有这些留给任务执行本身,并在此时一起删除withContext.

executeTasks与您的代码的有效不同之处是通过Mutex同步对mTaskList的访问.Mutex提供了一种简单的锁定机制,可以保证用withLock包装的代码一次只能执行一个(即atomically).由于withLock是一个挂起函数,因此所有其他try 运行该代码的协程都将挂起并等待当前运行的协程完成,然后才能执行包装的代码.

如果从不同的协程而不是从导致IndexOutOfBoundsException的竞争条件同时调用,您最终将看到多个单独的任务队列并行执行,而Mutex同步其对列表的访问.不过,自从"the list usually contains only one element"以来,这不会产生太大影响.但是,如果您需要保证不能同时执行两个任务,则应该将整个executeTasks包裹在Mutex的withLock中.

有了Mutex,您还应该将对列表的所有other访问打包在withLock块中(显然,对于同一Mutex),尤其是将新元素插入列表中的代码.

Android相关问答推荐

ArrayList.remove()(Kotlin中的removeAt())导致奇怪的IndexOutOfBoundsResponse异常

无法理解Kotlin Coroutines and Flows中的J.C.编程行为

在Android Studio中陷入了不兼容的Gradle版本的循环

在Android Studio Iguana上运行示例代码时,Gradle Build错误

滑动以更改合成中的活动

Android设备.Net Maui上的mp3文件列表

在不增加父行宽度的情况下添加延迟行或可滚动行

如何检测低性能 Android 设备进行条件动画渲染?

如何在ExecutorService中设置progressBar的进度?不想使用 AsyncTask,因为它已被弃用

如何在 Jetpack Compose 中对齐按钮底部中心?

在 jetpack compose 中交替使用 View.INVISIBLE

在 Jetpack Compose 中重用具有重复代码的列

Jetpack Compose UI - 在 AlertDialog 中单击时按钮宽度会发生变化

组成不重叠的元素

使用 capacitor cordova 插件的 Android Studio 错误

在jetpack compose中将图像添加到脚手架顶部栏

在alert 对话框生成器中启动协程

如何从我的 android 应用程序中删除 QUERY_ALL_PACKAGES 权限?

Android WebView 没有在第一次页面完成时从本地存储读取数据?

如何满足设备内框架的无效 Wear OS 屏幕截图Wear OS 表盘策略违规?