Threads vs Coroutines dans Kotlin

1. Introduction

Dans ce tutoriel rapide, nous allons créer et exécuter des threads dans Kotlin.

Plus tard, nous discuterons de la manière de l'éviter complètement, en faveur de Kotlin Coroutines.

2. Création de threads

Créer un thread dans Kotlin est similaire à le faire en Java.

Nous pourrions soit étendre la classe Thread (bien que cela ne soit pas recommandé car Kotlin ne prend pas en charge l'héritage multiple):

class SimpleThread: Thread() { public override fun run() { println("${Thread.currentThread()} has run.") } }

Ou nous pouvons implémenter l' interface Runnable :

class SimpleRunnable: Runnable { public override fun run() { println("${Thread.currentThread()} has run.") } }

Et de la même manière que nous le faisons en Java, nous pouvons l'exécuter en appelant la méthode start () :

val thread = SimpleThread() thread.start() val threadWithRunnable = Thread(SimpleRunnable()) threadWithRunnable.start()

Alternativement, comme Java 8, Kotlin prend en charge les conversions SAM, nous pouvons donc en profiter et passer un lambda:

val thread = Thread { println("${Thread.currentThread()} has run.") } thread.start()

2.2. Fonction Kotlin thread ()

Une autre façon est de considérer la fonction thread () que Kotlin fournit:

fun thread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit ): Thread

Avec cette fonction, un thread peut être instancié et exécuté simplement par:

thread(start = true) { println("${Thread.currentThread()} has run.") }

La fonction accepte cinq paramètres:

  • start - Pour exécuter immédiatement le thread
  • isDaemon - Pour créer le thread en tant que thread démon
  • contextClassLoader - Un chargeur de classe à utiliser pour charger des classes et des ressources
  • name - Pour définir le nom du thread
  • priority - Pour définir la priorité du thread

3. Kotlin Coroutines

Il est tentant de penser que générer plus de threads peut nous aider à exécuter plus de tâches simultanément. Malheureusement, ce n'est pas toujours vrai.

La création d'un trop grand nombre de threads peut en fait entraîner une sous-performance d'une application dans certaines situations; les threads sont des objets qui imposent une surcharge lors de l'allocation d'objets et du garbage collection.

Pour surmonter ces problèmes, Kotlin a introduit une nouvelle façon d'écrire du code asynchrone et non bloquant; la Coroutine.

Semblable aux threads, les coroutines peuvent s'exécuter simultanément, attendre et communiquer entre elles, à la différence que leur création est bien moins chère que les threads.

3.1. Contexte Coroutine

Avant de présenter les générateurs de coroutine que Kotlin fournit prêts à l'emploi, nous devons discuter du contexte de Coroutine.

Les coroutines s'exécutent toujours dans un contexte qui est un ensemble d'éléments divers.

Les principaux éléments sont:

  • Travail - modélise un flux de travail annulable avec plusieurs états et un cycle de vie qui culmine dans son achèvement
  • Dispatcher - détermine le ou les threads que la coroutine correspondante utilise pour son exécution. Avec le dispatcher, nous pouvons limiter l'exécution de la coroutine à un thread spécifique, la distribuer à un pool de threads ou la laisser s'exécuter sans restriction

Nous verrons comment spécifier le contexte pendant que nous décrivons les coroutines dans les étapes suivantes.

3.2. lancement

La fonction de lancement est un constructeur de coroutine qui démarre une nouvelle coroutine sans bloquer le thread actuel et renvoie une référence à la coroutine en tant qu'objet Job :

runBlocking { val job = launch(Dispatchers.Default) { println("${Thread.currentThread()} has run.") } }

Il a deux paramètres facultatifs:

  • context - Le contexte dans lequel la coroutine est exécutée, s'il n'est pas défini, il hérite du contexte du CoroutineScope à partir duquel il est lancé
  • start - Les options de démarrage de la coroutine. Par défaut, l'exécution de la coroutine est immédiatement planifiée

Notez que le code ci-dessus est exécuté dans un pool d'arrière-plan partagé de threads car nous avons utilisé Dispatchers.Default qui le lance dans GlobalScope.

Alternativement, nous pouvons utiliser GlobalScope.launch qui utilise le même répartiteur:

val job = GlobalScope.launch { println("${Thread.currentThread()} has run.") }

Lorsque nous utilisons Dispatchers.Default ou GlobalScope.launch, nous créons une coroutine de premier niveau. Même s'il est léger, il consomme toujours des ressources mémoire pendant son exécution.

Au lieu de lancer des coroutines dans le GlobalScope, comme nous le faisons habituellement avec les threads (les threads sont toujours globaux), nous pouvons lancer des coroutines dans le cadre spécifique de l'opération que nous effectuons:

runBlocking { val job = launch { println("${Thread.currentThread()} has run.") } }

Dans ce cas, nous commençons une nouvelle coroutine dans le constructeur de coroutine runBlocking (que nous décrirons plus tard) sans spécifier le contexte. Ainsi, la coroutine héritera du contexte de runBlocking .

3.3. asynchrone

Une autre fonction fournie par Kotlin pour créer une coroutine est async .

La fonction async crée une nouvelle coroutine et renvoie un résultat futur en tant qu'instance de Deferred:

val deferred = async { [email protected] "${Thread.currentThread()} has run." }

deferred is a non-blocking cancellable future which describes an object that acts as a proxy for a result that is initially unknown.

Like launch, we can specify a context in which to execute the coroutine as well as a start option:

val deferred = async(Dispatchers.Unconfined, CoroutineStart.LAZY) { println("${Thread.currentThread()} has run.") }

In this case, we've launched the coroutine using the Dispatchers.Unconfined which starts coroutines in the caller thread but only until the first suspension point.

Note that Dispatchers.Unconfined is a good fit when a coroutine does not consume CPU time nor updates any shared data.

In addition, Kotlin provides Dispatchers.IO that uses a shared pool of on-demand created threads:

val deferred = async(Dispatchers.IO) { println("${Thread.currentThread()} has run.") }

Dispatchers.IO is recommended when we need to do intensive I/O operations.

3.4. runBlocking

We had an earlier look at runBlocking, but now let's talk about it in more depth.

runBlocking is a function that runs a new coroutine and blocks the current thread until its completion.

By way of example in the previous snippet, we launched the coroutine but we never waited for the result.

In order to wait for the result, we have to call the await() suspend method:

// async code goes here runBlocking { val result = deferred.await() println(result) }

await() is what’s called a suspend function. Suspend functions are only allowed to be called from a coroutine or another suspend function. For this reason, we have enclosed it in a runBlocking invocation.

Nous utilisons runBlocking dans les fonctions principales et dans les tests afin de pouvoir lier le code de blocage à d'autres écrits en style de suspension.

De la même manière que nous l'avons fait dans d'autres générateurs de coroutine, nous pouvons définir le contexte d'exécution:

runBlocking(newSingleThreadContext("dedicatedThread")) { val result = deferred.await() println(result) }

Notez que nous pouvons créer un nouveau thread dans lequel nous pourrions exécuter la coroutine. Cependant, un thread dédié est une ressource coûteuse. Et, lorsqu'il n'est plus nécessaire, nous devrions le publier ou mieux le réutiliser dans toute l'application.

4. Conclusion

Dans ce didacticiel, nous avons appris à exécuter du code asynchrone et non bloquant en créant un thread.

Comme alternative au fil, nous avons également vu comment l'approche de Kotlin pour l'utilisation des coroutines est simple et élégante.

Comme d'habitude, tous les exemples de code présentés dans ce didacticiel sont disponibles à l'adresse over sur Github.