Sean McQuillan
Sean McQuillan

Follow

30 abr, 2019 – 9 min read

Esto es parte de una serie de varias partes sobre el uso de Coroutines en Android. Este post se centra en cómo funcionan las coroutines y qué problemas resuelven.

Otros artículos de esta serie:

Las coroutines de Kotlin introducen un nuevo estilo de concurrencia que se puede utilizar en Android para simplificar el código asíncrono. Aunque son nuevas en Kotlin en la versión 1.3, el concepto de coroutines ha existido desde los albores de los lenguajes de programación. El primer lenguaje que exploró el uso de coroutines fue Simula en 1967.

En los últimos años, las coroutines han crecido en popularidad y ahora están incluidas en muchos lenguajes de programación populares como Javascript, C#, Python, Ruby y Go, por nombrar algunos. Las coroutines de Kotlin se basan en conceptos establecidos que se han utilizado para construir grandes aplicaciones.

En Android, las coroutines son una gran solución a dos problemas:

  1. Las tareas de larga ejecución son tareas que tardan demasiado en bloquear el hilo principal.
  2. La seguridad del hilo principal permite asegurar que cualquier función de suspensión puede ser llamada desde el hilo principal.

¡Vamos a sumergirnos en cada una de ellas para ver cómo las coroutines pueden ayudarnos a estructurar el código de una forma más limpia!

Tareas de larga ejecución

Tomar una página web o interactuar con una API implican hacer una petición de red. Del mismo modo, la lectura de una base de datos o la carga de una imagen del disco implican la lectura de un archivo. Este tipo de cosas son lo que yo llamo tareas de larga duración, tareas que tardan demasiado tiempo para que tu aplicación se detenga a esperarlas.

Puede ser difícil entender la rapidez con la que un teléfono moderno ejecuta código en comparación con una petición de red. En un Pixel 2, un solo ciclo de la CPU tarda algo menos de 0,0000000004 segundos, un número bastante difícil de entender en términos humanos. Sin embargo, si piensas en una solicitud de red como un parpadeo, alrededor de 400 milisegundos (0,4 segundos), es más fácil entender la rapidez con la que opera la CPU. En un parpadeo, o en una petición de red algo lenta, la CPU puede ejecutar más de mil millones de ciclos.

En Android, cada aplicación tiene un hilo principal que se encarga de manejar la interfaz de usuario (como dibujar vistas) y coordinar las interacciones del usuario. Si hay demasiado trabajo en este hilo, la aplicación parece colgarse o ralentizarse, lo que conduce a una experiencia de usuario no deseada. Cualquier tarea de larga duración debe realizarse sin bloquear el hilo principal, para que tu aplicación no muestre lo que se llama «jank», como animaciones congeladas, o responda lentamente a los eventos táctiles.

Para realizar una solicitud de red fuera del hilo principal, un patrón común son los callbacks. Las devoluciones de llamada proporcionan un mango a una biblioteca que puede utilizar para llamar a su código en algún momento futuro. Con las devoluciones de llamada, la búsqueda de developer.android.com podría tener este aspecto:

class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}

Aunque get se llama desde el hilo principal, utilizará otro hilo para realizar la solicitud de red. Entonces, una vez que el resultado esté disponible en la red, la llamada de retorno será llamada en el hilo principal. Esta es una gran manera de manejar las tareas de larga ejecución, y las bibliotecas como Retrofit pueden ayudarle a hacer peticiones de red sin bloquear el hilo principal.

Usando coroutines para tareas de larga ejecución

Las coroutines son una forma de simplificar el código utilizado para gestionar las tareas de larga ejecución como fetchDocs. Para explorar cómo las coroutinas simplifican el código de las tareas de larga ejecución, reescribamos el ejemplo de callback anterior para utilizar coroutinas.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

¿No bloquea este código el hilo principal? ¿Cómo devuelve un resultado de get sin esperar la petición de red y sin bloquearse? Resulta que las coroutines proporcionan una manera para que Kotlin ejecute este código y nunca bloquee el hilo principal.

Las coroutines se basan en las funciones regulares añadiendo dos nuevas operaciones. Además de invocar (o llamar) y devolver, las coroutinas añaden suspender y reanudar.

  • suspender – pausar la ejecución de la coroutina actual, guardando todas las variables locales
  • reanudar – continuar una coroutina suspendida desde el lugar en que fue pausada

Esta funcionalidad es añadida por Kotlin mediante la palabra clave suspend en la función. Sólo puede llamar a las funciones de suspensión desde otras funciones de suspensión, o mediante el uso de un constructor de coroutina como launch para iniciar una nueva coroutina.

Suspender y reanudar trabajan juntos para reemplazar las devoluciones de llamada.

En el ejemplo anterior, get suspenderá la coroutina antes de que inicie la solicitud de red. La función get seguirá siendo responsable de ejecutar la petición de red fuera del hilo principal. Entonces, cuando la solicitud de red se completa, en lugar de llamar a una devolución de llamada para notificar al hilo principal, puede simplemente reanudar la coroutina que suspendió.

Animación que muestra cómo Kotlin implementa suspender y reanudar para reemplazar los callbacks.

Mirando cómo se ejecuta fetchDocs, puedes ver cómo funciona suspender. Cada vez que una coroutina se suspende, el marco de la pila actual (el lugar que Kotlin utiliza para llevar la cuenta de qué función se está ejecutando y sus variables) se copia y se guarda para más adelante. Cuando se reanuda, el marco de la pila se copia de nuevo desde donde se guardó y comienza a ejecutarse de nuevo. En medio de la animación – cuando todas las coroutines en el hilo principal están suspendidas, el hilo principal está libre para actualizar la pantalla y manejar los eventos del usuario. Juntos, suspender y reanudar reemplazar las devoluciones de llamada. Cuando todas las coroutines del hilo principal se suspenden, el hilo principal queda libre para realizar otras tareas.

Aunque hayamos escrito un código secuencial directo que se parece a una petición de red bloqueante, las coroutines ejecutarán nuestro código exactamente como queremos y evitarán el bloqueo del hilo principal.

A continuación, vamos a echar un vistazo a cómo utilizar coroutines para la seguridad del hilo principal y explorar los despachadores.

Seguridad del hilo principal con coroutines

En las coroutines de Kotlin, las funciones de suspensión bien escritas son siempre seguras para llamar desde el hilo principal. No importa lo que hagan, siempre deben permitir que cualquier hilo las llame.

Pero, hay un montón de cosas que hacemos en nuestras aplicaciones Android que son demasiado lentas para suceder en el hilo principal. Las solicitudes de red, el análisis de JSON, la lectura o la escritura de la base de datos, o incluso simplemente iterar sobre grandes listas. Cualquiera de ellos tiene el potencial de ejecutarse lo suficientemente lento como para causar un «jank» visible para el usuario y debería ejecutarse fuera del hilo principal.

Usar suspend no le dice a Kotlin que ejecute una función en un hilo de fondo. Vale la pena decir claramente y a menudo que las coroutines se ejecutarán en el hilo principal. De hecho, es una muy buena idea usar Dispatchers.Main.immediate cuando se lanza una coroutina en respuesta a un evento de UI – de esa manera, si no se termina haciendo una tarea de larga duración que requiera de seguridad principal, el resultado puede estar disponible en el siguiente frame para el usuario.

Las coroutines se ejecutarán en el hilo principal, y suspender no significa estar en segundo plano.

Para hacer que una función que realiza un trabajo demasiado lento para el hilo principal sea main-safe, puedes decirle a las coroutines de Kotlin que realicen el trabajo en el despachador Default o IO. En Kotlin, todas las coroutines deben ejecutarse en un dispatcher – incluso cuando se ejecutan en el hilo principal. Las coroutines pueden suspenderse a sí mismas, y el dispatcher es lo que sabe cómo reanudarlas.

Para especificar dónde deben ejecutarse las coroutines, Kotlin proporciona tres Dispatchers que puedes utilizar para el despacho de hilos.

+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+

* Room proporcionará seguridad principal automáticamente si utilizas funciones de suspensión, RxJava o LiveData.

** Las librerías de red como Retrofit y Volley gestionan sus propios hilos y no requieren seguridad principal explícita en tu código cuando se utilizan con coroutines de Kotlin.

Para continuar con el ejemplo anterior, vamos a utilizar los despachadores para definir la función get. Dentro del cuerpo de get llamas a withContext(Dispatchers.IO) para crear un bloque que se ejecutará en el despachador IO. Cualquier código que pongas dentro de ese bloque se ejecutará siempre en el despachador IO. Dado que withContext es en sí misma una función de suspensión, funcionará utilizando coroutines para proporcionar seguridad principal.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.Main
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main

Con las coroutines puedes hacer el despacho de hilos con un control de grano fino. Como withContext te permite controlar en qué hilo se ejecuta cualquier línea de código sin introducir una devolución de llamada para devolver el resultado, puedes aplicarlo a funciones muy pequeñas como leer de tu base de datos o realizar una petición de red. Por lo tanto, una buena práctica es utilizar withContext para asegurarse de que cada función es segura para ser llamada en cualquier Dispatcher incluyendo Main – de esa manera la persona que llama nunca tiene que pensar en qué hilo será necesario para ejecutar la función.

En este ejemplo, fetchDocs se está ejecutando en el hilo principal, pero puede llamar con seguridad a get que realiza una solicitud de red en el fondo. Debido a que las coroutinas soportan suspender y reanudar, la coroutina en el hilo principal se reanudará con el resultado tan pronto como el bloque withContext se complete.

Las funciones de suspensión bien escritas son siempre seguras para llamar desde el hilo principal (o main-safe).

Es una muy buena idea hacer cada función de suspensión main-safe. Si hace algo que toca el disco, la red, o incluso sólo utiliza demasiada CPU, utilice withContext para hacerla segura para llamar desde el hilo principal. Este es el patrón que siguen las bibliotecas basadas en coroutines como Retrofit y Room. Si sigues este estilo en toda tu base de código, tu código será mucho más simple y evitarás mezclar preocupaciones de hilos con la lógica de la aplicación. Cuando se sigue de forma consistente, las coroutines son libres de lanzarse en el hilo principal y hacer peticiones a la red o a la base de datos con código simple mientras se garantiza que los usuarios no verán «jank».»

El rendimiento de withContext

withContext es tan rápido como los callbacks o RxJava para proporcionar seguridad principal. Incluso es posible optimizar las llamadas withContext más allá de lo que es posible con las devoluciones de llamada en algunas situaciones. Si una función hará 10 llamadas a una base de datos, puedes decirle a Kotlin que cambie una vez en un withContext externo alrededor de las 10 llamadas. Entonces, aunque la biblioteca de la base de datos llamará a withContext repetidamente, permanecerá en el mismo despachador y seguirá una ruta rápida. Además, el cambio entre Dispatchers.Default y Dispatchers.IO está optimizado para evitar los cambios de hilo siempre que sea posible.

Qué es lo siguiente

En este post hemos explorado qué problemas resuelven bien las coroutines. Las coroutines son un concepto realmente antiguo en los lenguajes de programación que se han popularizado recientemente debido a su capacidad para hacer más simple el código que interactúa con la red.

En Android, puedes utilizarlas para resolver dos problemas realmente comunes:

  1. Simplificar el código de las tareas de larga ejecución como la lectura de la red, el disco o incluso el análisis sintáctico de un resultado JSON grande.
  2. Realizar una seguridad principal precisa para asegurar que nunca se bloquea accidentalmente el hilo principal sin hacer que el código sea difícil de leer y escribir.

¡En el próximo post exploraremos cómo encajan en Android para hacer un seguimiento de todo el trabajo que se inició desde una pantalla! Dale una lectura:

Deja una respuesta

Tu dirección de correo electrónico no será publicada.