J'ai lu la documentation de Kotlin , et si j'ai bien compris les deux fonctions de Kotlin fonctionnent comme suit:
withContext(context)
: change le contexte de la coroutine courante, lorsque le bloc donné s'exécute, la coroutine revient au contexte précédent.async(context)
: Démarre une nouvelle coroutine dans le contexte donné et si nous appelons.await()
laDeferred
tâche retournée , elle suspendra la coroutine appelante et reprendra lorsque le bloc s'exécutant à l'intérieur de la coroutine engendrée reviendra.
Maintenant, pour les deux versions suivantes de code
:
Version 1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
Version 2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- Dans les deux versions block1 (), block3 () s'exécute dans le contexte par défaut (commonpool?) Où comme block2 () s'exécute dans le contexte donné.
- L'exécution globale est synchrone avec l'ordre block1 () -> block2 () -> block3 ().
- La seule différence que je vois est que version1 crée une autre coroutine, où en tant que version2 n'exécute qu'une seule coroutine lors du changement de contexte.
Mes questions sont:
N'est-il pas toujours préférable d'utiliser
withContext
plutôt queasync-await
car il est fonctionnellement similaire, mais ne crée pas une autre coroutine. Un grand nombre de coroutines, bien que légères, peut encore poser problème dans les applications exigeantes.Y a-t-il un cas
async-await
plus préférablewithContext
?
Mise à jour:
Kotlin 1.2.50 a maintenant une inspection de code où il peut convertir async(ctx) { }.await() to withContext(ctx) { }
.
kotlin
kotlin-coroutines
Mangat Rai Modi
la source
la source
withContext
, une nouvelle coroutine est toujours créée indépendamment. C'est ce que je peux voir à partir du code source.async/await
également une nouvelle coroutine, selon OP?Réponses:
Je voudrais dissiper ce mythe du "trop de coroutines" étant un problème en quantifiant leur coût réel.
Tout d'abord, nous devons démêler la coroutine elle-même du contexte de coroutine auquel elle est attachée. Voici comment vous créez juste une coroutine avec une surcharge minimale:
GlobalScope.launch(Dispatchers.Unconfined) { suspendCoroutine<Unit> { continuations.add(it) } }
La valeur de cette expression est un
Job
maintien d'une coroutine suspendue. Pour conserver la suite, nous l'avons ajoutée à une liste dans le cadre plus large.J'ai comparé ce code et conclu qu'il alloue 140 octets et prend 100 nanosecondes à compléter. Voilà donc à quel point une coroutine est légère.
Pour la reproductibilité, voici le code que j'ai utilisé:
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>>()
Ce code démarre un tas de coroutines, puis se met en veille pour que vous ayez le temps d'analyser le tas avec un outil de surveillance comme VisualVM. J'ai créé les classes spécialisées
JobList
etContinuationList
parce que cela facilite l'analyse du vidage du tas.Pour obtenir une histoire plus complète, j'ai utilisé le code ci-dessous pour également mesurer le coût de
withContext()
etasync-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() } }
C'est la sortie typique que j'obtiens du code ci-dessus:
Just launch: 140 nanoseconds launch and withContext : 520 nanoseconds launch and async-await: 1100 nanoseconds
Oui, cela
async-await
prend environ deux fois plus de tempswithContext
, mais ce n'est toujours qu'une microseconde. Vous auriez à les lancer dans une boucle serrée, ne faisant presque rien d'autre, pour que cela devienne "un problème" dans votre application.En utilisant,
measureMemory()
j'ai trouvé le coût de mémoire suivant par appel:Just launch: 88 bytes withContext(): 512 bytes async-await: 652 bytes
Le coût de
async-await
est exactement 140 octets plus élevé quewithContext
le nombre que nous avons obtenu en tant que poids de la mémoire d'une coroutine. Ceci ne représente qu'une fraction du coût total de mise en place duCommonPool
contexte.Si l'impact performances / mémoire était le seul critère pour choisir entre
withContext
etasync-await
, la conclusion devrait être qu'il n'y a pas de différence pertinente entre eux dans 99% des cas d'utilisation réels.La vraie raison est qu'une
withContext()
API plus simple et plus directe, notamment en termes de gestion des exceptions:async { ... }
entraîne l'annulation de son travail parent. Cela se produit indépendamment de la façon dont vous gérez les exceptions de la correspondanceawait()
. Si vous ne l'avez pas préparécoroutineScope
, cela peut faire tomber votre application entière.withContext { ... }
simplement levée par l'withContext
appel, vous la gérez comme n'importe quelle autre.withContext
se trouve également être optimisé, tirant parti du fait que vous suspendez la coroutine parent et attendez l'enfant, mais ce n'est qu'un bonus supplémentaire.async-await
devrait être réservé aux cas où vous voulez réellement la concurrence, de sorte que vous lanciez plusieurs coroutines en arrière-plan et n'attendiez qu'ensuite sur elles. En bref:async-await-async-await
- ne fais pas ça, utilisewithContext-withContext
async-async-await-await
- c'est la façon de l'utiliser.la source
async-await
: Lorsque nous utilisonswithContext
, une nouvelle coroutine est également créée (pour autant que je puisse le voir à partir du code source), alors pensez-vous que la différence peut venir d'ailleurs?async
crée unDeferred
objet, qui peut également expliquer une partie de la différence.Thread.destroy()
- l'exécution s'évanouissant dans les airs.Vous devez utiliser async / await lorsque vous souhaitez exécuter plusieurs tâches simultanément, par exemple:
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 vous n'avez pas besoin d'exécuter plusieurs tâches simultanément, vous pouvez utiliser withContext.
la source
En cas de doute, rappelez-vous ceci comme une règle de base:
Si plusieurs tâches doivent se produire en parallèle et que le résultat final dépend de leur achèvement, utilisez
async
.Pour renvoyer le résultat d'une seule tâche, utilisez
withContext
.la source
async
et lewithContext
blocage sont-ils dans une portée suspendue?async
etwithContext
que vous ne bloquez pas le thread principal, ils ne suspendront le corps de la coroutine que pendant une longue tâche en cours d'exécution et en attente d'un résultat. Pour plus d'informations et d'exemples, consultez cet article sur Medium: Opérations asynchrones avec Kotlin Coroutines .