110 votes

Kotlin : withContext() vs Async-await

J'ai lu docs kotlin Si j'ai bien compris, les deux fonctions Kotlin fonctionnent comme suit :

  1. withContext(context) : change le contexte de la coroutine en cours, lorsque le bloc donné s'exécute, la coroutine revient au contexte précédent.
  2. async(context) : Démarre une nouvelle coroutine dans le contexte donné et si nous appelons .await() sur l'article retourné Deferred il suspendra la coroutine appelante et reprendra lorsque le bloc s'exécutant à l'intérieur de la coroutine engendrée reviendra.

Voici maintenant les deux versions suivantes de code :

Version1 :

  launch(){
    block1()
    val returned = async(context){
      block2()
    }.await()
    block3()
  }

Version2 :

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. Dans les deux versions, block1(), block3() s'exécutent dans le contexte par défaut (commonpool ?) alors que block2() s'exécute dans le contexte donné.
  2. L'exécution globale est synchrone avec l'ordre bloc1() -> bloc2() -> bloc3().
  3. La seule différence que je vois est que la version 1 crée une autre coroutine, alors que la version 2 n'exécute qu'une seule coroutine en changeant de contexte.

Mes questions sont les suivantes :

  1. N'est-il pas toujours préférable d'utiliser withContext plutôt que async-await car elle est fonctionnellement similaire, mais ne crée pas une autre coroutine. Un grand nombre de coroutines, bien que légères, peuvent toujours poser problème dans les applications exigeantes.

  2. Existe-t-il un cas async-await est préférable à withContext ?

Mise à jour : Kotlin 1.2.50 dispose désormais d'un code d'inspection qui lui permet de convertir async(ctx) { }.await() to withContext(ctx) { } .

0 votes

Je pense que lorsque vous utilisez withContext une nouvelle coroutine est toujours créée. C'est ce que je vois dans le code source.

0 votes

@stdout ne le fait pas async/await créer également une nouvelle coroutine, selon l'OP ?

141voto

Marko Topolnik Points 77257

Le grand nombre de coroutines, bien que légères, peut encore poser problème dans les applications exigeantes.

J'aimerais dissiper ce mythe selon lequel "trop de coroutines" est un problème en quantifiant leur coût réel.

Tout d'abord, il convient de démêler les coroutine de l'Union européenne et de l'Union européenne. contexte de la coroutine à laquelle elle est rattachée. C'est ainsi que l'on crée une coroutine avec un minimum de frais généraux :

GlobalScope.launch(Dispatchers.Unconfined) {
    suspendCoroutine<Unit> {
        continuations.add(it)
    }
}

La valeur de cette expression est un Job en maintenant une coroutine suspendue. Pour conserver la continuation, nous l'avons ajoutée à une liste dans le cadre plus large.

J'ai analysé ce code et j'ai conclu qu'il allouait 140 octets et prend 100 nanosecondes à compléter. C'est donc dire la légèreté d'une coroutine.

Pour des raisons de 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 certain nombre de coroutines, puis s'endort afin que vous ayez le temps d'analyser le tas à l'aide d'un outil de surveillance tel que VisualVM. J'ai créé les classes spécialisées JobList y ContinuationList car cela facilite l'analyse du vidage du tas.


Pour obtenir un tableau plus complet, j'ai utilisé le code ci-dessous pour mesurer également le coût des 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()
    }
}

Voici le résultat typique que j'obtiens avec le code ci-dessus :

Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds

Oui, async-await prend environ deux fois plus de temps que l withContext mais ce n'est qu'une microseconde. Il faudrait les lancer dans une boucle serrée, en ne faisant presque rien d'autre, pour que cela devienne "un problème" dans votre application.

Utilisation measureMemory() J'ai trouvé les coûts de mémoire suivants par appel :

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes

Le coût de la async-await est exactement 140 octets plus haut que withContext le nombre que nous avons obtenu comme poids mémoire d'une coroutine. Ce n'est qu'une fraction du coût total de la mise en place de l'application CommonPool contexte.

Si l'impact sur les performances et la mémoire était le seul critère pour choisir entre withContext y async-await La conclusion devrait être qu'il n'y a pas de différence pertinente entre les deux dans 99 % des cas d'utilisation réelle.

La vraie raison est que withContext() une API plus simple et plus directe, notamment en termes de gestion des exceptions :

  • Une exception qui n'est pas gérée dans le cadre de async { ... } entraîne l'annulation de son job parent. Cela se produit quelle que soit la manière dont vous gérez les exceptions provenant de la fonction de correspondance await() . Si vous n'avez pas préparé de coroutineScope pour cela, cela peut faire chuter toute votre candidature.
  • Une exception qui n'est pas traitée dans le cadre du withContext { ... } est tout simplement lancé par la fonction withContext vous le traitez comme n'importe quel autre.

withContext est également optimisée, car elle tire parti du fait que vous suspendez la coroutine parentale et attendez la coroutine enfant, mais ce n'est qu'un avantage supplémentaire.

async-await devrait être réservé aux cas où l'on veut vraiment de la concurrence, de sorte que l'on lance plusieurs coroutines en arrière-plan et que l'on n'attende que sur elles. En résumé :

  • async-await-async-await - ne le faites pas, utilisez withContext-withContext
  • async-async-await-await - c'est ainsi qu'il faut l'utiliser.

0 votes

En ce qui concerne le coût supplémentaire de la mémoire de async-await : Lorsque nous utilisons withContext Une nouvelle coroutine est également créée (d'après ce que je peux voir dans le code source), alors pensez-vous que la différence puisse venir d'ailleurs ?

1 votes

@stdout La bibliothèque a évolué depuis que j'ai effectué ces tests. Le code dans la réponse est censé être entièrement autonome, essayez de l'exécuter à nouveau pour le valider. async crée un Deferred cela peut aussi expliquer une partie de la différence.

0 votes

~" Conserver la suite ". Quand devons-nous conserver cette information ?

31voto

Dmitry Points 1172

N'est-il pas toujours préférable d'utiliser withContext plutôt que asynch-await car il est similaire sur le plan fonctionnel, mais ne crée pas une autre coroutine. Un grand nombre de coroutines, bien que légères, peuvent toujours être un problème dans les applications exigeantes.

Y a-t-il un cas où asynch-await est préférable à withContext ?

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.

20voto

En cas de doute, il s'agit d'une règle empirique :

  1. Si plusieurs tâches doivent être exécutées en parallèle et que le résultat final dépend de l'achèvement de chacune d'entre elles, il convient d'utiliser la fonction async .

  2. Pour renvoyer le résultat d'une seule tâche, utilisez withContext .

1 votes

Sont-ils tous les deux async y withContext blocage dans un champ d'application suspendu ?

3 votes

@IgorGanapolsky Si vous parlez du blocage du fil principal, async y withContext ne bloqueront pas le thread principal, elles ne feront que suspendre le corps de la coroutine pendant qu'une tâche de longue durée est en cours d'exécution et attend un résultat. Pour plus d'informations et d'exemples, voir cet article sur Medium : Opérations asynchrones avec les coroutines Kotlin .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X