Kotlin Coroutines : bases

Les coroutines sont un moyen de faire de l’asynchrone. Annoncés comme un « thread léger » par JetBrains, elles permettent de faire des centaines d’opérations avec un nombre de ressources limités, en effet, un Thread permettra de faire tourner un certain nombre de coroutines.

Un thread prend en moyenne 1/2 Mb, une coroutine autour de 10kb.
Avec les coroutines on n’a pas seulement une diminution des ressources utilisées mais une autre manière de gerer l’asynchrone, que personnellement je trouve plus clair.

Synchrone

fun main(){
print("1")
print("2")
print("3")
}
// resultat : 123

Ce code est synchrone, chaque print s’exécutera après le précédent.

Si je change un peu mon code…

fun main(){
print("1")
printdelayed("2")
print("3")
}

fun printDelayed(number: String) {
Thread.sleep(milliseconds: 1000)
print(number)
}
// resultat : 123

Le résultat sera le mêmes au qu’il y aura eu une latence d’une seconde entre le 1 et le 2. Lors de cette latence, le Thread était bloqué.
Continuons nos modifications…

fun main(){
print("1")
runBlocking {
printdelayed("2")
}

print("3")
}

suspend fun printDelayed(number: String) {
delay(1000)
print(number)
}
// resultat : 123

Le résultat sera le même, le thread sera bloqué. Cependant on utilise désormais des coroutines pour effectuer ce rendu.

Qu’est-ce que runBlocking ? C’est un constructeur qui va nous permettre de lancer du code suspendu.
Il nous faut donc un constructeur pour lancer une fonction suspendu, cependant pour lancer une autre fonction suspendue depuis la premiere pas besoin de recréer un constructeur.

runBlocking -> suspend -> suspend -> suspend …

Il y en a plusieurs : runBlocking, launch, async. Pour le moment regardons runBlocking.

Imaginons que l’on veille que tout les prints s’affiches chacune avec une seconde de délai ?
Est-ce qu’on doit vraiment se retrouver avec ça ? :

runBlocking {
printdelayed("1")
}
runBlocking {
printdelayed("2")
}
runBlocking {
printdelayed("3")
}

Réponse : non, l’un des intérêts des coroutines c’est justement de limiter la verbosité des callbacks. On peut donc simplifier tout ça :

fun main() = runBlocking {
printdelayed("1")
printdelayed("2")
printdelayed("3")
print("4")
}

Toutes les fonctions de main, même le dernier print, seront effectués dans un même Thread, en l’occurence, comme il n’est pas précisé dans l’appel du runBlocking, le main thread.
Mais il est possible de sélectionner d’autres threads pour ne pas utiliser le main à l’aide de Dispatchers, nous allons voir ça plus tard.
Pour savoir quel thread est utilisé dans une méthode il suffit d’utiliser : Thread.currentThread().name

Asynchrone

Maintenant que nous avons vu la manière de faire du synchrone en coroutine, nous allons voir comment faire de l’asynchrone.
Reprenons le code que nous avions et changeons pour une coroutine non bloquante.

Nous utilisons ici le GlobalScope.launch. Le GlobalScope permet de lancer des coroutines qui fonctionnent pendant toute la durée de vie de l’application.

fun main(){
print("1")
GlobalScope.launch {
printdelayed("2")
}
print("3")
}

suspend fun printdelayed(s: String) {
delay(1000)
println(s)
}
//Resultat : 13

On découvre ici Launch , c’est un fire & forget. Par défaut, sans context précisé, un Launch se lancera dans un thread différent. C’est ce qui le différencie du Runblock qui par défaut tourne dans le main thread.

Pourquoi le 2 ne s’affiche pas ? Nous ne sommes plus dans un environnement bloquant, on appel al a suite le print du 1, la fonction du launch, puis le print du 3.

Comme il y a un délai d’une seconde pour affiche le print du 2 et que le programme a continué de son côté a afficher le prenait du 3 pour arriver ensuite à la fin du programme il n’a donc pas le temps d’afficher le 2 puisque le programme à fini de s’executer.
Petit resumé graphique :

C’est un peu comme si le chiffre 2 était arrivé à la soirée alors que tout le monde est déjà partit. Il existe bien mais personne ne l’aura vu quand il y avait de l’action.

Alors comment faire pour que 2 soit là ? On pourrai ajouter un delay après print 3, 2 s’afficherai mais soyons honnête, c’est sale.

fun main() = runBlocking{
print("1")
val job = GlobalScope.launch {
printDelayed("2")
}
print("3")
job.join()
}
//Resultat : 132

Pour faire en sorte que le programme se finisse lorsque le launch a fini sa propre execution, il faudra récupérer l’objet retourné par le launch soit un Job.
On pourra appeler donc le job avec job.join(). Etant donné que join() est une suspend function, il faudra donc englober le tout avec un runBlocking.
Il sera possible de simplifier comme ceci :

fun main() = runBlocking{
print("1")
launch {
printdelayed("2")
}
print("3")
}
//Resultat : 132

C’est mieux non ? déjà plus agréable à lire, et plus besoin de lancer manuellement le job. Le résultat de cette fonction sera donc le même que le précédent.

Durant chacun de ses exemples nous bloquions le main Thread en faisant des operations synchrones. Maintenant nous allons voir l’asynchrone et limiter le bloquage du main thread, ce qui est une TRES mauvaise idée en Android, pour rappel, en Android le main thread EST l’UI thread. Effectuer des longues opérations résultera en un crash de l’application de type ANR.
Voir la documentation officielle.

Voici donc comment faire fonctionner le launch dans un thread différent du main/ui thread.

fun main() = runBlocking {
print("1")
launch(Dispatchers.Default) {
printdelayed("2 "+Thread.currentThread().name)
}
print("3")
}

Ici nous précisons que le launch refera sur le Dispatcher par défaut. Qu’est-ce qu’un dispatcher ?
Un dispatcher de par sa traduction est un répartiteur. Donc notre cas il va déterminé quel thread va être utilisé par notre coroutine. Il y a plusieurs dispatch :

Default :
C’est un pool de threads en arriere plan. Utiliser Default reviens à ne pas préciser le dispatcher et laisser le globalscope et son join.
Main :
Comme son nom l’indique, la coroutine fonctionnera dans le main thread/UI thread. Ne fonctionne que dans le cadre d’une application Android, il nécessitera des librairies en plus. On l’utilisera pour effectuer des actions sur les objets de l’UI.
Unconfined :
N’est pas lié a un thread spécifique, il va généralement hérité du thread de son contexte actuel.
IO :
La coroutine utilisera des threads créés à la demande et spécifiques à la gestion intense de IO.

Il est aussi possible de créer ses propres dispatchers, notamment à partier des Schedulers de Rx.

En résumé :

Coroutines : thread légé
Runblocking : tourne par défait dans le main thread
Suspend Function : processus suspendu qui sera lancé depuis une coroutine, elles ne block pas le thread appelant, elle peut appeler d’autre fonctions suspend.
GlobalScope.launch : tourne par défaut dans un thread different.
Dispatcher : permet de preciser l’environnement de thread ou s’exécutera le code d’un constructeur.