I have a service that holds a list of trains (important data that I want to share between coroutines). It also has methods to modify the list. So, I use mutex in these methods. This is the code that calls the methods (it is inside a coroutine):
val contours: List<MatOfPoint> = ArrayList()
val hierarchy = Mat()
Imgproc.findContours(result, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE)
Imgproc.cvtColor(result, result, Imgproc.COLOR_GRAY2BGR)
for (contour in contours) {
if (contour.toArray().size > 100) {
val mask = Mat.zeros(foreground.size(), CvType.CV_8UC1)
val white = Scalar.all(255.0)
Imgproc.drawContours(mask, listOf(contour), -1, white, -1)
val meanColor = Core.mean(foreground, mask)
trainService.addTrain(contour, meanColor)
Imgproc.drawContours(result, listOf(contour), -1, meanColor, FILLED)
} else {
Imgproc.drawContours(result, listOf(contour), -1, Scalar(0.0, 0.0, 0.0), FILLED)
}
}
trainService.removeUnusedTrains()
val resultBitmap =
Bitmap.createBitmap(result.width(), result.height(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(result, resultBitmap)
This is the train service
class TrainService {
private var trains : MutableList<Train> = emptyList<Train>().toMutableList()
private val mutex = Mutex()
suspend fun addTrain(newTrain: MatOfPoint, trainColor: Scalar) = mutex.withLock {
for(train in trains) {
when {
train.isSame(newTrain) -> return true
}
}
val train = Train(newTrain, trainColor)
trains.add(train)
}
suspend fun removeUnusedTrains() = mutex.withLock {
for(train in trains) {
if (!train.changed) {
trains.remove(train)
} else {
train.changed = false
}
}
Log.d("TrainService", "Trains now: ${trains.joinToString()}")
}
}
I expect that to lock it so only one at the time can modify or read the list. But when I try to run it I get this error:
time: 1709989371723
msg: java.util.ConcurrentModificationException
stacktrace: java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
at com.xcerna11.traincontroller.TrainService.removeUnusedTrains(TrainService.kt:25)
at com.xcerna11.traincontroller.OpenCVDetector$run$2.invokeSuspend(OpenCVDetector.kt:74)
at com.xcerna11.traincontroller.OpenCVDetector$run$2.invoke(Unknown Source:8)
at com.xcerna11.traincontroller.OpenCVDetector$run$2.invoke(Unknown Source:4)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
at com.xcerna11.traincontroller.OpenCVDetector.run(OpenCVDetector.kt:24)
at com.xcerna11.traincontroller.TrainAnalyzer$analyze$1$1.invokeSuspend(TrainAnalyzer.kt:20)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:284)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source:1)
at com.xcerna11.traincontroller.TrainAnalyzer.analyze(TrainAnalyzer.kt:19)
at androidx.camera.core.ImageAnalysis.lambda$setAnalyzer$2(ImageAnalysis.java:528)
at androidx.camera.core.ImageAnalysis$$ExternalSyntheticLambda2.analyze(D8$$SyntheticClass:0)
at androidx.camera.core.ImageAnalysisAbstractAnalyzer.lambda$analyzeImage$0$androidx-camera-core-ImageAnalysisAbstractAnalyzer(ImageAnalysisAbstractAnalyzer.java:286)
at androidx.camera.core.ImageAnalysisAbstractAnalyzer$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
at java.lang.Thread.run(Thread.java:1012)
The problem does not come from the mutex, but from the fact that you call remove in a for each loop. The list is modified while being browsed, which it does not support.
To make your remove function work, you should use a listIterator, which supports modifications at the same time than looping:
suspend fun removeUnusedTrains() = mutex.withLock {
val iter = traines.listIterator()
while (iter.hasNext()) {
val train = iter.next()
if (!train.changed) {
// Ask iterator to remove current element from the list
iter.remove()
} else {
train.changed = false
}
}
Log.d("TrainService", "Trains now: ${trains.joinToString()}")
}