web-dev-qa-db-fra.com

Kotlin: withContext () vs Async-wait

J'ai été documentation kotlin et 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é est exécuté, 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 la tâche retournée Deferred, il suspend la coroutine appelante et reprend lorsque le bloc qui s'exécute dans la coroutine générée est renvoyé. .

Passons maintenant aux 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écute dans le contexte par défaut (commonpool?) Où as () s'exécute dans le contexte donné.
  2. L'exécution globale est synchrone avec l'ordre block1 () -> block2 () -> block3 ().
  3. La seule différence que je vois est que version1 crée une autre coroutine, où version2 n'exécute qu'une coroutine lors du changement de contexte.

Mes questions sont:

  1. N’est-il pas toujours préférable d’utiliser withContext plutôt que async-await car il est fonctionnellement similaire, mais ne crée pas d’autre coroutine. Un grand nombre de routines, bien que léger, pourrait toujours poser problème dans des applications exigeantes.

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

Mise à jour: Kotlin 1.2.5 a maintenant une inspection du code permettant de convertir async(ctx) { }.await() to withContext(ctx) { }.

59
Mangat Rai Modi

Un grand nombre de routines, bien que léger, pourrait toujours poser problème dans des applications exigeantes

Je voudrais dissiper ce mythe selon lequel "trop ​​de coroutines" pose problème en quantifiant leur coût réel.

Tout d’abord, nous devrions démêler la coroutine du contexte de la coroutine auquel c'est attaché. Voici comment créer une coroutine avec un minimum de temps système:

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

La valeur de cette expression est un Job tenant une coroutine suspendue. Pour conserver la suite, nous l'avons ajoutée à une liste de portée plus large.

J'ai comparé ce code et conclu qu'il alloue 140 octets et prend 100 nanosecondes à compléter. Voilà comment 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 sommeil afin que vous ayez le temps d'analyser le segment de mémoire à l'aide d'un outil de surveillance tel que VisualVM. J'ai créé les classes spécialisées JobList et ContinuationList car cela facilite l'analyse du vidage de segment de mémoire.


Pour obtenir une histoire plus complète, j'ai utilisé le code ci-dessous pour mesurer également le coût de withContext() et 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 du 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 withContext, mais il ne reste qu'une microseconde. Il faudrait les lancer en boucle, en ne faisant presque rien, pour que cela devienne "un problème" dans votre application.

En utilisant measureMemory() j'ai trouvé le coût en mémoire par appel suivant:

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

Le coût de async-await est exactement supérieur de 140 octets à withContext, le nombre que nous avons obtenu comme poids mémoire d’un coroutine. Il ne s'agit que d'une fraction du coût total de la configuration du contexte CommonPool.

Si l'impact performance/mémoire était le seul critère permettant de décider entre withContext et async-await, il faudrait en conclure qu'il n'y a pas de différence significative entre eux dans 99% des cas d'utilisation réels.

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 async { ... } provoque l'annulation de son travail parent. Cela se produit quelle que soit la façon dont vous gérez les exceptions de la await() correspondante. Si vous n'avez pas préparé de coroutineScope pour cela, votre application risque de tomber.
  • Une exception non gérée dans withContext { ... } est simplement levée par l'appel withContext, vous la gérez comme une autre.

withContext est également optimisé, en exploitant le fait que vous suspendez la coroutine parent et que vous attendez l'enfant, mais ce n'est qu'un bonus supplémentaire.

async-await devrait être réservé aux cas où vous souhaitez réellement une concurrence, de manière à lancer plusieurs coroutines en arrière-plan et à ne les attendre ensuite. En bref:

  • async-await-async-await - identique à withContext-withContext
  • async-async-await-await - c'est la façon de l'utiliser.
88
Marko Topolnik

N’est-il pas toujours préférable d’utiliser withContext plutôt que d’asynch-wait car il est fonctionnellement similaire, mais ne crée pas une autre coroutine. Coroutines de grands nombres, bien que léger pourrait toujours être un problème dans les applications exigeantes

Y at-il un cas asynch-wait est préférable à withContext

Vous devez utiliser async/wait 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.

14
Dmitry