Kotlin / Native multithreading usando corrutinas
He tenido una oportunidad en la multiplataforma de kotlin y es brillante, pero el enhebrado me desconcierta. La congelación del estado entre subprocesos tiene sentido conceptualmente y funciona bien en ejemplos simples donde pequeños objetos o primitivas se pasan de un lado a otro, pero en aplicaciones del mundo real no puedo evitar InvalidMutabilityException.
Tome el siguiente fragmento de código común de una aplicación de Android
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
)
private var coreroutineSupervisor = SupervisorJob()
private var coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main + coreroutineSupervisor)
private fun loadResults() {
// Here: Show loading
coroutineScope.launch {
try {
val result = withContext(Dispatchers.Default) { objectWhichContainsNetworking.fetchData() }
// Here: Hide loading and show results
} catch (e: Exception) {
// Here: Hide loading and show error
}
}
}
Nada muy complejo, pero si se usa en código común y se ejecuta desde Kotlin / Native, entonces pow InvalidMutabilityException en MainViewModel.
Parece que la razón de esto es que todo lo que se pasa en withContext se congela de forma recursiva, por lo que objectWhichContainsNetworking es una propiedad de MainViewModel y se usa en withContext, luego MainViewModel queda atrapado en la congelación.
Entonces, mi pregunta es, ¿es esto solo una limitación del modelo actual de memoria Kotlin / Native? ¿O quizás la versión actual de las corrutinas? ¿Y hay alguna forma de evitar esto?
Nota: versión de corrutinas: 1.3.9-native-mt. kotlin versión 1.4.0.
Edición 1: Parece que el código reducido anterior realmente funciona bien. Resulta que el código incriminatorio era una var actualizable en el modelo de vista (usado para mantener una referencia al último estado de vista) que se congela y luego lanza la excepción cuando intenta mutar. Voy a intentar usar Flow / Channels para asegurarme de que no se necesita una referencia de var y ver si esto soluciona el problema general.
Nota: si hay una manera de evitar que MainViewModel se congele en primer lugar, ¡sería fantástico!
Edición 2: reemplazó la var con Flow. No pude obtener la recopilación de flujo estándar en iOS hasta que usé los ayudantes aquí:https://github.com/JetBrains/kotlinconf-app/blob/master/common/src/mobileMain/kotlin/org/jetbrains/kotlinconf/FlowUtils.kt.
MainViewModel todavía se congela, pero como todo su estado es inmutable, ya no es un problema. ¡Espero que ayude a alguien!
Respuestas
En su código original, hace referencia a un campo del objeto principal, lo que hace que capture el elemento principal completo y lo congele. No es un problema con las corrutinas. Coroutines sigue las mismas reglas que todas las demás bibliotecas de concurrencia en Kotlin / Native. Congela la lambda cuando cruzas hilos.
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
)
//yada yada
private fun loadResults() {
coroutineScope.launch {
try {
val result = withContext(Dispatchers.Default) {
//The reference to objectWhichContainsNetworking is a field ref and captures the whole view model
objectWhichContainsNetworking.fetchData()
}
} catch (e: Exception) {}
}
}
Para evitar que esto suceda:
class MainViewModel(
private val objectWhichContainsNetworking: ObjectWhichContainsNetworking
){
init{
ensureNeverFrozen()
}
//Etc
Lo más complicado con el modelo de memoria es esto. Acostumbrarse a lo que se captura y evitarlo. No es tan difícil cuando te acostumbras, pero necesitas aprender los conceptos básicos.
He hablado de esto extensamente:
Práctica Kotlin / Concurrencia nativa
Prácticas de simultaneidad nativa de Kotlin
Concurrencia KN de KotlinConf
El modelo de memoria está cambiando, pero pasará bastante tiempo antes de que aterrice. Una vez que se acostumbre al modelo de memoria, los problemas inmutables generalmente son fáciles de diagnosticar.