Общее состояние, меняемое из двух корутин, портится на iOS, но не на Android
Этот общий код собирает аналитические события. На Android он ведёт себя нормально; на iOS отчёт иногда возвращается неполным, а изредка приложение падает внутри списка. Никаких исключений про потоки не летит, и ни один объект в модуле не заморожен.
Ограничение: правка обязана жить в commonMain и дать обеим целям одну и ту же гарантию. Не уводите работу на единственный платформенный поток со стороны iOS.
// commonMain
class EventCollector {
private val events = mutableListOf<String>()
suspend fun record(event: String) = withContext(Dispatchers.Default) {
events.add(event)
}
fun report(): List<String> = events.toList()
}
Определите причину и исправьте.
Современный менеджер памяти Kotlin/Native позволяет потокам делить изменяемые объекты, поэтому никто и не жалуется: это обычная гонка данных, ровно как на JVM. Заморозки больше нет, и она не решение — защитите состояние из общего кода диспетчером или Mutex.
- ✗Хвататься за
freeze()— он запрещён на уровне ошибки и никогда не был примитивом синхронизации - ✗Считать, что Kotlin/Native до сих пор не даёт двум потокам менять один объект
- ✗Чинить это на стороне iOS, хотя незащищённое состояние лежит в
commonMainи Android тоже гоняет
- →Почему тот же код на Android выглядит работающим куда чаще, чем на iOS?
- →Когда сведение к одному диспетчеру предпочтительнее взятия
Mutex?
Диагноз
Старый менеджер памяти Kotlin/Native действительно запрещал делить изменяемое состояние между потоками, и попытка это сделать падала. Этой модели больше нет. Современный рантайм — трассирующий сборщик мусора, и он спокойно позволяет двум потокам держать один объект. freeze() при этом никуда не делся из библиотеки, но помечен устаревшим на уровне ошибки и в любом случае никогда не был примитивом синхронизации.
Значит, никакой особой «нативной» ошибки тут нет: mutableListOf не потокобезопасен, Dispatchers.Default — это пул потоков, и две корутины пишут в events одновременно. Это обычная гонка данных. Она есть и на Android — просто там реализация ArrayList и планировщик чаще прощают.
Правка
// commonMain
class EventCollector {
private val mutex = Mutex()
private val events = mutableListOf<String>()
suspend fun record(event: String) = withContext(Dispatchers.Default) {
mutex.withLock { events.add(event) }
}
suspend fun report(): List<String> = mutex.withLock { events.toList() }
}
Mutex из kotlinx.coroutines приостанавливает корутину вместо блокировки потока и живёт в commonMain, поэтому гарантия одинакова на обеих целях. Альтернатива — свести все изменения к одному однопоточному диспетчеру и не делить состояние вовсе.