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
withContextlugar deasync-awaitya 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-awaitsea 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/awaitcrea 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
Jobmantener 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
JobListyContinuationListporque 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 nanosecondsSí,
async-awaittoma 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 bytesEl costo de
async-awaites exactamente 140 bytes más alto quewithContextel número que obtuvimos como el peso de memoria de una corrutina. Esto es solo una fracción del costo total de configurar elCommonPoolcontexto.Si el impacto en el rendimiento / memoria fuera el único criterio para decidir entre
withContextyasync-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 unacoroutineScopepara ello, es posible que se elimine toda la aplicación.withContext { ... }simplemente es lanzada por lawithContextllamada, usted la maneja como cualquier otra.withContexttambié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-awaitdebe 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-withContextasync-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?asynccrea unDeferredobjeto, 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
asyncy elwithContextbloqueo en un ámbito de suspensión?asyncywithContextno 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 .