He estado leyendo documentos de Kotlin , y si entendí correctamente, las dos funciones de Kotlin funcionan de la siguiente manera:
withContext(context)
: cambia el contexto de la corrutina actual, cuando se ejecuta el bloque dado, la corrutina vuelve al contexto anterior.async(context)
: Inicia una nueva corrutina en el contexto dado y si invocamos la tarea.await()
devueltaDeferred
, suspenderá la corrutina de llamada y se reanudará cuando regrese el bloque que se ejecuta dentro de la corrutina 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 ejecutan en el contexto predeterminado (commonpool?) Donde como block2 () 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 version1 crea otra corrutina, mientras que version2 ejecuta solo una corrutina mientras cambia de contexto.
Mis preguntas son:
¿No es siempre mejor usar en
withContext
lugar deasync-await
ya que es funcionalmente similar, pero no crea otra corrutina? Un gran número de corrutinas, aunque livianas, aún podrían ser un problema en aplicaciones exigentes.¿Hay algún caso que
async-await
sea más preferiblewithContext
?
Actualización:
Kotlin 1.2.50 ahora tiene una inspección de código donde puede convertir async(ctx) { }.await() to withContext(ctx) { }
.
kotlin
kotlin-coroutines
Mangat Rai Modi
fuente
fuente
withContext
, siempre se crea una nueva corrutina independientemente. Esto es lo que puedo ver en el código fuente.async/await
crea también una nueva corrutina, según OP?Respuestas:
Me gustaría disipar este mito de que "demasiadas corrutinas" son un problema cuantificando su costo real.
Primero, debemos desenredar la corrutina en sí del contexto de la corrutina al que está adjunta. Así es como crea solo una corrutina con una sobrecarga mínima:
GlobalScope.launch(Dispatchers.Unconfined) { suspendCoroutine<Unit> { continuations.add(it) } }
El valor de esta expresión es
Job
mantener una corrutina suspendida. Para retener la continuación, la agregamos a una lista en el alcance más amplio.Evalué este código y concluí que asigna 140 bytes y tarda 100 nanosegundos en completarse. Así de ligera es una corrutina.
Para la 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 corrutinas y luego duerme para que tenga tiempo de analizar el montón con una herramienta de monitoreo como VisualVM. Creé las clases especializadas
JobList
yContinuationList
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()
yasync-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() } }
Este es el resultado típico que obtengo del código anterior:
Just launch: 140 nanoseconds launch and withContext : 520 nanoseconds launch and async-await: 1100 nanoseconds
Sí,
async-await
toma aproximadamente el doble de tiempowithContext
, pero sigue siendo solo un microsegundo. Tendría que ejecutarlos en un bucle cerrado, sin hacer casi nada además, 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 quewithContext
el número que obtuvimos como el peso de memoria de una corrutina. Esto es solo una fracción del costo total de configurar elCommonPool
contexto.Si el impacto en el rendimiento / memoria fuera el único criterio para decidir entre
withContext
yasync-await
, la conclusión tendría que ser que no hay una diferencia relevante entre ellos en el 99% de los casos de uso reales.La verdadera razón es que
withContext()
una API más simple y directa, especialmente en términos de manejo de excepciones:async { ... }
hace que se cancele su trabajo principal. Esto sucede independientemente de cómo maneje las excepciones de la coincidenciaawait()
. Si no ha preparado unacoroutineScope
para ello, es posible que se elimine toda la aplicación.withContext { ... }
simplemente es lanzada por lawithContext
llamada, usted la maneja como cualquier otra.withContext
también está optimizado, aprovechando el hecho de que está suspendiendo la corrutina principal y esperando al niño, pero eso es solo una ventaja adicional.async-await
debe reservarse para aquellos casos en los que realmente desea la concurrencia, de modo que inicie varias corrutinas en segundo plano y solo luego las espere. En breve:async-await-async-await
- no hagas eso, usawithContext-withContext
async-async-await-await
- esa es la forma de usarlo.fuente
async-await
: Cuando usamoswithContext
, también se crea una nueva corrutina (por lo que puedo ver en el código fuente), así que ¿cree que la diferencia podría provenir de otro lugar?async
crea unDeferred
objeto, que también puede explicar algunas de las diferencias.Thread.destroy()
: la ejecución se desvanece en el aire.Debe usar async / await cuando desee ejecutar varias tareas al mismo tiempo, 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 al mismo tiempo, puede usar withContext.
fuente
En caso de duda, recuerde esto como una regla general:
Si tienen que realizarse varias tareas en paralelo y el resultado final depende de que se completen todas, utilice
async
.Para devolver el resultado de una sola tarea, utilice
withContext
.fuente
async
y elwithContext
bloqueo en un ámbito de suspensión?async
ywithContext
no bloqueará el hilo principal, solo suspenderán el cuerpo de la corrutina mientras se ejecuta una tarea de larga duración y espera un resultado. Para obtener más información y ejemplos, consulte este artículo sobre Medio: operaciones asíncronas con Kotlin Coroutines .