Coroutines :: experiencia práctica

En este artículo hablaré sobre cómo funcionan las corutinas y cómo crearlas. Considere la aplicación en ejecución secuencial y paralela. Hablemos sobre el manejo de errores, la depuración y las formas de probar la rutina. Al final, resumiré y hablaré sobre las impresiones que quedaron después de aplicar este enfoque.

El artículo fue preparado en base a los materiales de mi informe sobre MBLT DEV 2018 , al final de la publicación, un enlace al video.

Estilo consistente



Fig. 2.1

¿Cuál fue el propósito de los desarrolladores de Corutin? Querían que la programación asincrónica fuera lo más simple posible. No hay nada más fácil que ejecutar el código "línea por línea" utilizando las construcciones sintácticas del lenguaje: try-catch-finally, bucles, sentencias condicionales, etc.

Consideremos dos funciones. Cada uno se ejecuta en su propio hilo (Fig. 2.1). El primero se ejecuta en el subproceso B y devuelve algunos datos de resultados B , luego debemos pasar este resultado a la segunda función, que toma los datos B como argumento y ya se está ejecutando en el subproceso A. Con la rutina, podemos escribir nuestro código como se muestra en la fig. 2.1. Considere cómo lograr esto.

Funciones longOpOnB, longOpOnA : las llamadas funciones de suspensión , antes de las cuales se libera el subproceso y, una vez completado su trabajo, vuelve a estar ocupado.

Para que estas dos funciones se realicen en un subproceso diferente en relación con el llamado, manteniendo un estilo de código de escritura "coherente", debemos sumergirlas en el contexto de la rutina.

Esto se hace mediante la creación de corutinas utilizando el llamado Coroutine Builder. En la figura, este es el lanzamiento , pero hay otros, por ejemplo, async , runBlocking . Hablaré de ellos más tarde.

El último argumento es un bloque de código ejecutado en el contexto de la rutina: llamar a las funciones de suspensión, lo que significa que todo el comportamiento anterior solo es posible en el contexto de la rutina o en otra función de suspensión.

Hay otros parámetros en el método Coroutine Builder, por ejemplo, el tipo de lanzamiento, el hilo en el que se ejecutará el bloque y otros.

Gestión del ciclo de vida


Coroutine Builder nos da el valor de retorno como un valor de retorno, una subclase de la clase Job (Fig.2.2). Con él, podemos gestionar el ciclo de vida de la corutina.

Comience con el método start () , cancele con el método cancel () , espere a que se complete el trabajo utilizando el método join ( ), suscríbase al evento de finalización del trabajo y más.


Fig. 2.2 2.2

Cambio de flujo


Puede cambiar el flujo de ejecución de la rutina cambiando el elemento de contexto de la rutina que es responsable de la programación. (Fig. 2.3)

Por ejemplo, corutin 1 se ejecutará en un subproceso de interfaz de usuario , mientras que corutin 2 en un subproceso tomado del grupo Dispatchers.IO .


Fig.2.3

La biblioteca de corutina también proporciona una función de suspensión conContext (CoroutineContext) , con la que puede cambiar entre hilos en el contexto de una corutina. Por lo tanto, saltar entre hilos puede ser bastante simple:


Fig. 2.4.

Comenzamos nuestra rutina en el hilo 1 de la IU → muestra el indicador de carga → cambiamos al hilo 2 de trabajo, liberando el hilo principal → realizamos una operación larga allí que no se puede realizar en el hilo UI → regresamos el resultado al hilo 3 de la IU → y ya trabajamos allí con él, renderizando los datos recibidos y ocultando el indicador de carga.

Parece bastante cómodo hasta ahora, sigue adelante.

Función de suspensión


Considere el trabajo de corutina en el ejemplo del caso más común: trabajar con solicitudes de red utilizando la biblioteca Retrofit 2.

Lo primero que debemos hacer es convertir la llamada de devolución de llamada en una función de suspensión para aprovechar la función de rutina:


Fig. 2.5

Para controlar el estado de la rutina, la biblioteca proporciona funciones de la forma suspendXXXXCoroutine , que proporciona un argumento que implementa la interfaz Continuación , utilizando los métodos resumeWithException y resume de los cuales podemos reanudar la rutina en caso de error y éxito, respectivamente.

A continuación, descubriremos qué sucede cuando se llama al método resumeWithException y, primero, nos aseguramos de que de alguna manera debemos cancelar la llamada de solicitud de red.

Función de suspensión. Cancelación de la llamada


Para cancelar la llamada y otras acciones relacionadas con la liberación de recursos no utilizados, al implementar la función de suspensión, puede usar el método suspendCancellableCoroutine que sale de la caja (Fig. 2.6). Aquí, el argumento de bloque ya implementa la interfaz CancellableContinuation , uno de los métodos adicionales de los cuales, invokeOnCancellation , le permite registrarse para un error o un evento de cancelación de rutina exitoso. Por lo tanto, aquí también es necesario cancelar la llamada al método.


Fig. 2.6

Mostrar cambios en la interfaz de usuario


Ahora que la función de suspensión se ha preparado para las solicitudes de red, puede utilizar su llamada en la secuencia Coroutine UI como secuencial, mientras que durante la ejecución de la solicitud la secuencia estará libre, y la secuencia de modificación se utilizará para la solicitud.

Por lo tanto, implementamos el comportamiento asíncrono con respecto a la secuencia de interfaz de usuario, pero lo escribimos en un estilo consistente (Fig. 2.6).

Si después de recibir la respuesta necesita hacer el trabajo duro, por ejemplo, escribir los datos recibidos en la base de datos, esta función, como ya se ha mostrado, se puede realizar fácilmente usando withContext en el conjunto de flujos de flujo de retorno y continuar la ejecución en la interfaz de usuario sin una sola línea de código.


Fig. 2.7

Desafortunadamente, esto no es todo lo que necesitamos para el desarrollo de aplicaciones. Considere el manejo de errores.

Manejo de errores: try-catch-finally. Cancelar la rutina: cancelaciónExcepción


Una excepción que no se detectó dentro de la rutina se considera no controlada y puede provocar el bloqueo de la aplicación. Además de las situaciones normales, se genera una excepción al reanudar la rutina utilizando el método resumeWithException en la línea correspondiente de la llamada a la función de suspensión. En este caso, la excepción aprobada como argumento se arroja sin cambios. (Fig. 2.8)


Fig. 2.8

Para el manejo de excepciones, está disponible la construcción estándar del lenguaje try catch finally. Ahora el código que puede mostrar el error en la interfaz de usuario toma la siguiente forma:


Fig. 2.9

En el caso de cancelar la rutina, que se puede lograr llamando al método de cancelación de Job #, se lanza una excepción de cancelación . Esta excepción se maneja de manera predeterminada y no genera fallas u otras consecuencias negativas.

Sin embargo, cuando se utiliza la construcción try / catch , quedará atrapada en el bloque catch , y debe tenerlo en cuenta en los casos si desea manejar solo situaciones realmente "erróneas". Por ejemplo, el manejo de errores en la interfaz de usuario cuando es posible "cancelar" las solicitudes o el registro de errores se proporciona. En el primer caso, el error se mostrará al usuario, aunque en realidad no existe, y en el segundo, se registrará una excepción inútil y desordenará los informes.

Para ignorar la situación de cancelar corutinas, debe modificar ligeramente el código:


Fig. 2.10

Error de registro


Considere la excepción excepción rastro de pila.

Si lanza una excepción directamente en el bloque de código de rutina (Fig. 2.11), el seguimiento de la pila se ve ordenado, con solo unas pocas llamadas de la rutina, indica correctamente la línea y la información sobre la excepción. En este caso, puede comprender fácilmente desde el seguimiento de la pila dónde exactamente, en qué clase y en qué función se produjo la excepción.


Fig. 2.11

Sin embargo, las excepciones que se pasan al método resumeWithException de las funciones de suspensión , por regla general, no contienen información sobre la rutina en la que se produjo. Por ejemplo (Fig. 2.12), si reanuda la rutina de la función de suspensión implementada previamente con la misma excepción que en el ejemplo anterior, el seguimiento de la pila no dará información sobre dónde buscar específicamente el error.


Fig. 2.12

Para comprender qué corutina se reanudó con una excepción, puede usar el elemento de contexto CoroutineName . (Fig. 2.13)

El elemento CoroutineName se usa para depurar, pasando el nombre de la corutina, puede extraerlo en las funciones de suspensión y, por ejemplo, complementar el mensaje de excepción. Es decir, al menos estará claro dónde buscar un error.

Este enfoque solo funcionará si la función de suspensión se excluye de esto:


Fig. 2,13

Error al iniciar sesión. ExceptionHandler


Para cambiar el registro de excepciones para una determinada rutina, puede configurar su propio ExceptionHandler, que es uno de los elementos del contexto de la rutina. (Fig. 2.14)

El controlador debe implementar la interfaz CoroutineExceptionHandler . Usando el operador anulado + para el contexto de rutina, puede reemplazar el controlador de excepción estándar por el suyo. La excepción no controlada caerá en el método handleException , donde puede hacer lo que necesite con él. Por ejemplo, ignorar por completo. Esto sucederá si deja el controlador vacío o agrega su propia información:


Fig. 2,14

Veamos cómo se vería el registro de nuestra excepción:

  1. Debe recordar sobre la excepción de cancelación , que queremos ignorar.
  2. Agregue sus propios registros.
  3. Recuerde sobre el comportamiento predeterminado, que incluye iniciar sesión y finalizar la aplicación, de lo contrario, la excepción simplemente "desaparecerá" y no quedará claro qué sucedió.

Ahora, para el caso de lanzar una excepción, se enviará una lista de seguimiento de la pila al logcat con la información agregada:


Fig. 2,15

Ejecución paralela. asíncrono


Considere la operación paralela de las funciones de suspensión.

Async es el más adecuado para organizar resultados paralelos de múltiples funciones. Asíncrono, como el lanzamiento : Coroutine Builder. Su conveniencia es que, utilizando el método await () , devuelve datos si tiene éxito o arroja una excepción que ha ocurrido durante la ejecución de la rutina. El método de espera esperará a que se complete la rutina, si aún no se ha completado, de lo contrario devolverá inmediatamente el resultado del trabajo. Tenga en cuenta que esperar es una función de suspensión y, por lo tanto, no se puede ejecutar fuera del contexto de una función de suspensión u otra rutina.

Usando asíncrono, obtener datos de dos funciones en paralelo se verá más o menos así:


Fig. 2,16

Imagine que nos enfrentamos a la tarea de obtener datos de dos funciones en paralelo. Luego, debes combinarlos y mostrarlos. En caso de error, es necesario dibujar la IU, cancelando todas las solicitudes actuales. Tal caso a menudo se encuentra en la práctica.

En este caso, el error debe manejarse de la siguiente manera:

  1. Traiga el manejo de errores dentro de cada async-corutin.
  2. En caso de error, cancele todas las corutinas. Afortunadamente, para esto es posible especificar un trabajo principal, tras la cancelación de la cual se cancelan todos sus hijos.
  3. Se nos ocurrió una implementación adicional para comprender si todos los datos se han cargado correctamente. Por ejemplo, suponemos que si la espera devuelve nulo, se produjo un error al recibir datos.

Con todo esto en mente, la implementación de la rutina de los padres se está volviendo un poco más complicada. La implementación de async-corutin también es complicada:


Fig. 2,17

Este enfoque no es el único posible. Por ejemplo, puede implementar la ejecución paralela con manejo de errores usando ExceptionHandler o SupervisorJob .

Corutinas Anidadas


Veamos el trabajo de la corutina anidada.

Por defecto, la rutina anidada se crea utilizando un ámbito externo y hereda su contexto. Como resultado, la rutina anidada se convierte en una hija y el padre externo.

Si cancelamos la rutina externa, las corridas anidadas creadas de esta manera, que se usaron en el ejemplo anterior, también se cancelarán. También será útil al salir de la pantalla cuando necesite cancelar las solicitudes actuales. Además, el padre corutin siempre esperará la finalización de la hija.

Puede crear una rutina que sea independiente de la externa utilizando un ámbito global. En este caso, cuando se cancela la rutina externa, la anidada continuará funcionando como si nada hubiera sucedido:


Fig. 2,18

Puede crear un elemento secundario de la rutina global anidada reemplazando el elemento de contexto con la clave Trabajo con el trabajo principal, o puede utilizar completamente el contexto de la rutina principal. Pero en este caso, vale la pena recordar que se toman todos los elementos de la rutina principal: el grupo de subprocesos, el controlador de excepciones, etc.


Fig. 2,19

Ahora está claro que si usa la rutina desde el exterior, debe proporcionarles la capacidad de instalar una instancia del trabajo o el contexto del padre. Y los desarrolladores de bibliotecas deben considerar la posibilidad de instalarlo de niño, lo que causa inconvenientes.

Puntos de corte


Las rutinas afectan la visualización de los valores de los objetos en modo de depuración. Si coloca un punto de interrupción dentro de la siguiente rutina en la función logData , cuando se dispara, vemos que todo está bien aquí y los valores se muestran correctamente:


Fig. 2,20

Ahora obtenga los datos A usando la rutina anidada, dejando un punto de interrupción en logData :


Fig. 2,21

Intentar expandir este bloque para intentar encontrar los valores deseados falla. Por lo tanto, la depuración en presencia de funciones suspendidas se vuelve difícil.

Prueba unitaria


Las pruebas unitarias son bastante sencillas. Puede usar el RunBlocking de Coroutine Builder para esto . runBlocking bloquea un subproceso hasta que terminen todas sus rutinas anidadas, que es exactamente lo que necesita para probar.

Por ejemplo, si se sabe que en algún lugar dentro del método se usa la rutina para implementarlo, entonces para probar el método solo necesita envolverlo en runBlocking .

runBlocking se puede usar para probar una función de suspensión:


Fig. 2,22

Ejemplos


Finalmente, me gustaría mostrar algunos ejemplos del uso de la corutina.

Imagine que necesitamos ejecutar tres consultas A, B y C en paralelo, mostrar su finalización y reflejar el momento de finalización de las solicitudes A y B.

Para hacer esto, simplemente puede envolver las consultas de las rutinas A y B en una común y trabajar con ella como un todo:


Fig. 2,23

El siguiente ejemplo muestra cómo usar el ciclo regular for para ejecutar consultas periódicas con un intervalo de 5 segundos:


Fig. 2,24

Conclusiones


De los inconvenientes, observo que las corutinas son una herramienta relativamente joven, por lo que si desea usarlas en la producción, debe hacerlo con precaución. Hay dificultades de depuración, un pequeño punto de referencia en la implementación de cosas obvias.

En general, las corutinas son bastante fáciles de usar, especialmente para implementar tareas asincrónicas no complicadas. En particular, debido al hecho de que se pueden utilizar construcciones de lenguaje estándar. Las corutinas son fácilmente susceptibles de pruebas unitarias y todo esto viene de la caja de la misma compañía que desarrolla el lenguaje.

Informar video


Resultó muchas cartas. Para aquellos a quienes les gusta escuchar más: video de mi informe sobre MBLT DEV 2018 :


Materiales útiles sobre el tema:


Source: https://habr.com/ru/post/442920/


All Articles