Tenho lido a documentação do Kotlin e, se entendi corretamente, as duas funções do Kotlin funcionam da seguinte maneira:
withContext(context)
: muda o contexto da co-rotina atual, quando o bloco dado é executado, a co-rotina volta para o contexto anterior.async(context)
: Inicia uma nova co-rotina no contexto dado e se chamarmos.await()
aDeferred
tarefa retornada , ela suspenderá a co-rotina de chamada e continuará quando o bloco em execução dentro da co-rotina gerada retornar.
Agora, para as seguintes duas versões de code
:
Versão 1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
Versão 2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- Em ambas as versões, block1 (), block3 () executa no contexto padrão (commonpool?) Onde como block2 () é executado no contexto dado.
- A execução geral é sincronizada com a ordem block1 () -> block2 () -> block3 ().
- A única diferença que vejo é que a versão1 cria outra co-rotina, enquanto a versão2 executa apenas uma co-rotina ao alternar o contexto.
Minhas perguntas são:
Não é sempre melhor usar
withContext
do queasync-await
porque é funcionalmente semelhante, mas não cria outra co-rotina. Um grande número de corrotinas, embora leves, ainda podem ser um problema em aplicativos exigentes.Existe um caso que
async-await
é mais preferívelwithContext
?
Atualização:
Kotlin 1.2.50 agora tem uma inspeção de código onde pode converter async(ctx) { }.await() to withContext(ctx) { }
.
kotlin
kotlin-coroutines
Mangat Rai Modi
fonte
fonte
withContext
, uma nova co-rotina é sempre criada independentemente. Isso é o que posso ver no código-fonte.async/await
cria também uma nova co-rotina, de acordo com OP?Respostas:
Eu gostaria de dissipar esse mito de que "muitas corrotinas" são um problema, quantificando seu custo real.
Primeiro, devemos separar a própria co - rotina do contexto da co - rotina ao qual ela está anexada. É assim que você cria apenas uma co-rotina com sobrecarga mínima:
GlobalScope.launch(Dispatchers.Unconfined) { suspendCoroutine<Unit> { continuations.add(it) } }
O valor desta expressão é
Job
manter uma co-rotina suspensa. Para manter a continuação, nós a adicionamos a uma lista em um escopo mais amplo.Eu comparei esse código e concluí que ele aloca 140 bytes e leva 100 nanossegundos para ser concluído. Então é assim que uma co-rotina é leve.
Para reprodutibilidade, este é o código que usei:
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 um monte de corrotinas e depois dorme para que você tenha tempo de analisar o heap com uma ferramenta de monitoramento como o VisualVM. Eu criei as classes especializadas
JobList
eContinuationList
porque isso torna mais fácil analisar o despejo de heap.Para obter uma história mais completa, usei o código abaixo para medir também o custo de
withContext()
easync-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 é a saída típica que obtenho do código acima:
Just launch: 140 nanoseconds launch and withContext : 520 nanoseconds launch and async-await: 1100 nanoseconds
Sim,
async-await
leva o dobro do tempowithContext
, mas ainda é apenas um microssegundo. Você teria que iniciá-los em um loop fechado, sem fazer quase nada além disso, para que isso se tornasse "um problema" em seu aplicativo.Usando
measureMemory()
, encontrei o seguinte custo de memória por chamada:Just launch: 88 bytes withContext(): 512 bytes async-await: 652 bytes
O custo de
async-await
é exatamente 140 bytes maior do quewithContext
o número que obtivemos como o peso da memória de uma co-rotina. Isso é apenas uma fração do custo total de configuração doCommonPool
contexto.Se o impacto no desempenho / memória fosse o único critério para decidir entre
withContext
easync-await
, a conclusão teria de ser que não há diferença relevante entre eles em 99% dos casos de uso reais.O verdadeiro motivo é que
withContext()
uma API mais simples e direta, especialmente em termos de tratamento de exceções:async { ... }
faz com que seu trabalho pai seja cancelado. Isso acontece independentemente de como você lida com as exceções da correspondênciaawait()
. Se você não preparou umcoroutineScope
para isso, ele pode desativar todo o seu aplicativo.withContext { ... }
simplesmente é lançada pelawithContext
chamada, você a trata como qualquer outro.withContext
também acontece de ser otimizado, aproveitando o fato de que você está suspendendo a co-rotina pai e aguardando o filho, mas isso é apenas um bônus adicional.async-await
deve ser reservado para aqueles casos em que você realmente deseja simultaneidade, de forma que você inicie várias corrotinas em segundo plano e só então espere por elas. Em resumo:async-await-async-await
- não faça isso, usewithContext-withContext
async-async-await-await
- essa é a maneira de usá-lo.fonte
async-await
: Quando usamoswithContext
, uma nova co-rotina também é criada (pelo que posso ver no código-fonte), então você acha que a diferença pode estar vindo de outro lugar?async
cria umDeferred
objeto, o que também pode explicar algumas das diferenças.Thread.destroy()
- a execução desaparecer no ar.Você deve usar async / await quando quiser executar várias tarefas simultaneamente, por exemplo:
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) }
Se você não precisa executar várias tarefas ao mesmo tempo, pode usar o withContext.
fonte
Em caso de dúvida, lembre-se disso como uma regra prática:
Se várias tarefas tiverem que acontecer em paralelo e o resultado final depender da conclusão de todas elas, use
async
.Para retornar o resultado de uma única tarefa, use
withContext
.fonte
async
e owithContext
bloqueio estão em um escopo suspenso?async
ewithContext
não bloqueará o thread principal, eles apenas suspenderão o corpo da co-rotina enquanto alguma tarefa de longa execução estiver em execução e aguardando um resultado. Para obter mais informações e exemplos, consulte este artigo sobre Meio: Operações assíncronas com corrotinas Kotlin .