J'ai été documentation kotlin et si j'ai bien compris, les deux fonctions kotlin fonctionnent comme suit:
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.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()
}
Mes questions sont:
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.
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) { }
.
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:
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.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.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.