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