lib - kotlin experimental coroutines enable
Kotlin: withContext() vs Async-await (2)
He sido kotlin docs y si entendí correctamente las dos funciones de kotlin funcionan de la siguiente manera:
-
withContext(context)
: cambia el contexto de la rutina actual, cuando el bloque dado se ejecuta, la rutina vuelve al contexto anterior. -
async(context)
: inicia una nueva coroutine en el contexto dado y si llamamosawait()
en la tareaDeferred
devuelta, suspenderá la coroutine llamante y se reanudará cuando vuelva a ejecutarse el bloqueo dentro de la coroutine generada.
Ahora para las siguientes dos versiones de code
:
Versión 1 :
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
Versión 2 :
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- En ambas versiones block1 (), block3 () se ejecuta en el contexto predeterminado (commonpool?) Donde as blocks () se ejecuta en el contexto dado.
- La ejecución general es sincrónica con el orden block1 () -> block2 () -> block3 ().
- La única diferencia que veo es que la versión 1 crea otra coroutine, donde la versión 2 ejecuta solo una coroutine al cambiar de contexto.
Mis preguntas son:
¿No es siempre mejor usar
withContext
lugar deasynch-await
ya que es funcionalmente similar, pero no crea otra rutina? Numerosas corutinas, aunque ligeras, aún podrían ser un problema en aplicaciones exigentes¿Hay un caso
asynch-await
es más preferible awithContext
Actualización: Kotlin 1.2.50 ahora tiene una inspección de código donde puede convertir async(ctx) { }.await() to withContext(ctx) { }
.
¿No es siempre mejor usar withContext en lugar de asynch-await, ya que es funcionalmente similar, pero no crea otra rutina? Numerosas corutinas, aunque ligeras, aún podrían ser un problema en aplicaciones exigentes
¿Hay un caso asynch-await es más preferible a withContext?
Debería usar async / await cuando desee ejecutar varias tareas a la vez, por ejemplo:
runBlocking {
val deferredResults = arrayListOf<Deferred<String>>()
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"1"
}
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"2"
}
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"3"
}
//wait for all results (at this point tasks are running)
val results = deferredResults.map { it.await() }
println(results)
}
Si no necesita ejecutar varias tareas a la vez, puede usar withContext.
Un gran número de coroutines, aunque ligeros, aún podrían ser un problema en aplicaciones exigentes
Me gustaría disipar este mito de que "demasiadas coroutinas" es un problema al cuantificar su costo real.
Primero, deberíamos desenredar la propia coroutina del contexto de la misma a la que se adjunta. Así es como creas solo una coroutine con un mínimo de sobrecarga:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
El valor de esta expresión es un Job
contiene una coroutina suspendida. Para conservar la continuación, la agregamos a una lista en el ámbito más amplio.
Hice una prueba comparativa de este código y llegué a la conclusión de que asigna 140 bytes y tarda 100 nanosegundos en completarse. Así que así de ligera es una coroutine.
Para reproducibilidad, este es el código que utilicé:
fun measureMemoryOfLaunch() {
val continuations = ContinuationList()
val jobs = (1..10_000).mapTo(JobList()) {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
}
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
class JobList : ArrayList<Job>()
class ContinuationList : ArrayList<Continuation<Unit>>()
Este código inicia un montón de coroutines y luego se duerme para que tenga tiempo de analizar el montón con una herramienta de monitoreo como VisualVM. JobList
clases especializadas JobList
y ContinuationList
porque esto facilita el análisis del volcado de pila.
Para obtener una historia más completa, utilicé el siguiente código para medir también el costo de withContext()
y async-await
:
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis
const val JOBS_PER_BATCH = 100_000
var blackHoleCount = 0
val threadPool = Executors.newSingleThreadExecutor()!!
val ThreadPool = threadPool.asCoroutineDispatcher()
fun main(args: Array<String>) {
try {
measure("just launch", justLaunch)
measure("launch and withContext", launchAndWithContext)
measure("launch and async", launchAndAsync)
println("Black hole value: $blackHoleCount")
} finally {
threadPool.shutdown()
}
}
fun measure(name: String, block: (Int) -> Job) {
print("Measuring $name, warmup ")
(1..1_000_000).forEach { block(it).cancel() }
println("done.")
System.gc()
System.gc()
val tookOnAverage = (1..20).map { _ ->
System.gc()
System.gc()
var jobs: List<Job> = emptyList()
measureTimeMillis {
jobs = (1..JOBS_PER_BATCH).map(block)
}.also { _ ->
blackHoleCount += jobs.onEach { it.cancel() }.count()
}
}.average()
println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds")
}
fun measureMemory(name:String, block: (Int) -> Job) {
println(name)
val jobs = (1..JOBS_PER_BATCH).map(block)
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
val justLaunch: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {}
}
}
val launchAndWithContext: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
withContext(ThreadPool) {
suspendCoroutine<Unit> {}
}
}
}
val launchAndAsync: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
async(ThreadPool) {
suspendCoroutine<Unit> {}
}.await()
}
}
Esta es la salida típica que obtengo del código anterior:
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
Sí, async-await
lleva aproximadamente el doble de tiempo que withContext
, pero sigue siendo solo un microsegundo. Tendría que lanzarlos en un circuito cerrado, sin hacer casi nada, para que eso se convierta en "un problema" en su aplicación.
Usando measureMemory()
encontré el siguiente costo de memoria por llamada:
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
El costo de async-await
es exactamente 140 bytes más alto que withContext
, el número que obtuvimos como el peso de memoria de una coroutine. Esto es solo una fracción del costo total de configurar el contexto de CommonPool
.
Si el impacto en el rendimiento / memoria fue el único criterio para decidir entre withContext
y async-await
withContext
, la conclusión sería que no hay una diferencia relevante entre ellos en el 99% de los casos de uso real.
La verdadera razón es que withContext()
una API más simple y directa, especialmente en términos de manejo de excepciones:
- Una excepción que no se maneja dentro de
async { ... }
hace que su trabajo principal se cancele. Esto sucede independientemente de cómo maneje las excepciones de la coincidenciaawait()
. Si no ha preparado uncoroutineScope
decoroutineScope
para él, puede derribar toda su aplicación. - Una excepción no manejada dentro de
withContext { ... }
simplemente es lanzada por la llamadawithContext
, usted la maneja como cualquier otra.
withContext
también está optimizado, aprovechando el hecho de que está suspendiendo la rutina de padres y esperando al hijo, pero eso es solo una ventaja adicional.
async-await
debe reservarse para aquellos casos en los que realmente desee la concurrencia, de modo que lance varias coroutines en segundo plano y solo las espere. En breve:
-
async-await-async-await
withContext-withContext
- igual quewithContext-withContext
-
async-async-await-await
- esa es la forma de usarlo.