Isla KotlinTextos anteriores de esta serie: sobre AsyncTask , sobre Loaders , sobre Executors y EventBus , sobre RxJava .Entonces esta hora ha llegado. Este es el artículo para el que se escribió toda la serie: una explicación de cómo funciona el nuevo enfoque "bajo el capó". Si aún no sabe cómo usarlo, aquí hay algunos enlaces útiles para comenzar:
Y habiendo dominado las corutinas, puede preguntarse qué permitió a Kotlin brindar esta oportunidad y cómo funciona. Tenga en cuenta que aquí nos centraremos solo en la etapa de compilación: puede escribir un artículo separado sobre la ejecución.
Lo primero que debemos entender es que, en el corpus, en realidad no existen corutinas. El compilador convierte la función con el modificador suspendido en una función con el parámetro
Continuación . Esta interfaz tiene dos métodos:
abstract fun resume(value: T) abstract fun resumeWithException(exception: Throwable)
El tipo T es el tipo de retorno de su función de suspensión original. Y esto es lo que sucede realmente: esta función se ejecuta en un determinado hilo (paciencia, también llegamos a esto), y el resultado se pasa a la función de reanudación de esa continuación, en cuyo contexto se llamó a la función de suspensión. Si la función no recibe el resultado y genera una excepción, se genera resumeWithException, arrojando un error al código de llamada.
Ok, pero ¿de dónde vino la continuación? ¡Por supuesto, del constructor Corutin! Veamos el código que crea cualquier rutina, por ejemplo, lanzar:
public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine }
Aquí, el creador crea una rutina: una instancia de la clase AbstractCoroutine, que, a su vez, implementa la interfaz de Continuación. El método de inicio pertenece a la interfaz de trabajo. Pero encontrar la definición del método de inicio es muy difícil. Pero podemos venir aquí desde el otro lado. Un lector atento ya ha notado que el primer argumento para la función de lanzamiento es CoroutineContext, y está configurado en DefaultDispatcher de forma predeterminada. Los despachadores son clases que controlan la ejecución de las rutinas, por lo que son definitivamente importantes para comprender lo que está sucediendo. Veamos la declaración DefaultDispatcher:
public actual val DefaultDispatcher: CoroutineDispatcher = CommonPool
Entonces, de hecho, esto es CommonPool, aunque los muelles Java nos dicen que esto puede cambiar. ¿Qué es CommonPool?
Este es un administrador de rutina que utiliza
ForkJoinPool como una implementación de ExecutorService. Sí, lo es: al final, todas sus corutinas lambda son simplemente Runnable, que entró en Ejecutor con un conjunto de transformaciones difíciles. Pero el diablo, como siempre, está en los detalles.
Tenedor? O unirse?A juzgar por los resultados de la encuesta en mi twitter, aquí necesito explicar brevemente qué es FJP :)
En primer lugar, ForkJoinPool es un ejecutor moderno creado para su uso con flujos paralelos Java 8. La tarea original era un paralelismo eficiente cuando se trabajaba con la API Stream, lo que esencialmente significa dividir los flujos para procesar parte de los datos y luego combinarlos cuando todos los datos se han procesado. Para simplificar, imagine que tiene el siguiente código:
IntStream .range(1, 1_000_000) .parallel() .sum()
La cantidad de dicha secuencia no se calculará en una secuencia, en su lugar, ForkJoinPool dividirá recursivamente el rango en partes (primero en dos partes de 500,000, luego cada una de ellas en 250,000, y así sucesivamente), calcule la suma de cada parte y combine los resultados en un solo cantidad Aquí hay una visualización de dicho proceso:
Los subprocesos se dividen para diferentes tareas y se fusionan nuevamente después de la finalizaciónLa efectividad de FJP se basa en el algoritmo de "robo de trabajo": cuando un subproceso particular se queda sin tareas, va a las colas de otros subprocesos de grupo y roba sus tareas. Para una mejor comprensión, puede ver el
informe de Alexei Shipilev o ver una
presentación .
¡Bueno, nos dimos cuenta de lo que están haciendo nuestras corutinas! ¿Pero cómo terminan allí?
Esto sucede dentro del método de despacho CommonPool #:
_pool.execute(timeSource.trackTask(block))
El método de despacho se llama desde el método resume (Valor: T) en DispatchedContinuation. Suena familiar! Recordamos que Continuation es una interfaz implementada en AbstractCoroutine. ¿Pero cómo se relacionan?
El truco está dentro de la clase CoroutineDispatcher. Implementa la interfaz ContinuationInterceptor de la siguiente manera:
public actual override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = DispatchedContinuation(this, continuation)
¿Ves? Usted proporciona un bloque simple al constructor de corutina. No necesita implementar ninguna interfaz de la que no quiera saber nada. La biblioteca de rutina se encarga de todo esto. Ella es
intercepta la ejecución, reemplaza la continuación con DispatchedContinuation y la envía al ejecutor, lo que garantiza la ejecución más eficiente de su código.
Ahora, lo único que debemos tratar es cómo se llama el despacho desde el método de inicio. Llenemos este vacío. El método de reanudación se llama desde startCoroutine en la función de extensión del bloque:
public fun <R, T> (suspend R.() -> T).startCoroutine( receiver: R, completion: Continuation<T> ) { createCoroutineUnchecked(receiver, completion).resume(Unit) }
Y startCoroutine, a su vez, es llamado por el operador "()" en la enumeración CoroutineStart. Su constructor lo acepta como el segundo parámetro, y el valor predeterminado es CoroutineStart.DEFAULT. Eso es todo!
Esa es la razón por la que admiro el enfoque de corutina: no es solo una sintaxis espectacular, sino también una implementación brillante.
Y para aquellos que han leído hasta el final, obtienen exclusivos: un video de mi informe "No se necesita un violinista: rechazamos RxJava a favor de la corutina en Kotlin" de la conferencia de Mobius . Disfruta :)