runblocking lib guide experimental enable coroutines await async kotlin kotlinx.coroutines kotlin-coroutines

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:

  1. withContext(context) : cambia el contexto de la rutina actual, cuando el bloque dado se ejecuta, la rutina vuelve al contexto anterior.
  2. async(context) : inicia una nueva coroutine en el contexto dado y si llamamos await() en la tarea Deferred 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() }

  1. En ambas versiones block1 (), block3 () se ejecuta en el contexto predeterminado (commonpool?) Donde as blocks () se ejecuta en el contexto dado.
  2. La ejecución general es sincrónica con el orden block1 () -> block2 () -> block3 ().
  3. 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:

  1. ¿No es siempre mejor usar withContext 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

  2. ¿Hay un caso asynch-await es más preferible a withContext

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 coincidencia await() . Si no ha preparado un coroutineScope de coroutineScope para él, puede derribar toda su aplicación.
  • Una excepción no manejada dentro de withContext { ... } simplemente es lanzada por la llamada withContext , 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 que withContext-withContext
  • async-async-await-await - esa es la forma de usarlo.