Parlons Multithreading en Flutter

Qui suis-je ?

Thomas Ecalle

Cyllene - Multithreading en Flutter

Sommaire

  1. Asynchronisme vs Parallélisme vs Concurrence
     

  2. Comment le langage Dart fonctionne ?
     

  3. Penchons-nous sur les Futures
     

  4. Le vrai Multithreading : les Isolate

Cyllene - Multithreading en Flutter

Asynchronisme
vs Parallélisme
vs Concurrence

Cyllene - Multithreading en Flutter

Asynchronisme

  • Toutes les activités I/O (Input / Output)
    • appels réseaux
    • écriture sur le disque
    •  ...
  • Votre programme n'est pas responsable du calcul
     
  • Exemple d'appels réseaux multiple

Définition Wikipédia :

asynchrony, in computer programming, refers to the occurence of events independent of the main program flow and ways to deal with such events.

Cyllene - Multithreading en Flutter

Parallélisme

Parallel computing is a type of computation in which many calculations or the execution of processes are carried out simultaneously. Large problems can often be divided into smaller ones, which can then be solved at the same time.

  • Utiliser les processeurs pour répartir le travail
  • Chaque processeur peut effectuer une partie du travail
  • A la fin, on regroupe l'ensemble des résultats
  • Exemple de traitement d'images

Définition Wikipédia :

Cyllene - Multithreading en Flutter

Concurrence

Concurrent computing is a form of computing in which several computations are executing during overlapping time periods - concurrently - instead of sequentially (one completing before the next starts).

  • Plusieurs services en parallèlle
  • Chaque service a une responsabilité propre
  • Ils peuvent (ou non) communiquer les uns avec les autres
  • Exemple de service de BDD, service de logs, etc.

Définition Wikipédia :

Cyllene - Multithreading en Flutter

Finalement

C'est une question de point de vue

  • Paralléliser l'algo ? Parallélisme
  • Paralléliser les accès à l'I/O ? Asynchronisme
  • Paralléliser les acteurs ? Concurrence

Cyllene - Multithreading en Flutter

Comment le langage Dart fonctionne ?

Cyllene - Multithreading en Flutter

Un langage Single Thread

  • Un seul Thread (Isolate) créé au démarrage de l’application
     

  • Dart n’exécute qu’une instruction à la fois

BLOQUANT

void myBigLoop(){
    for (int i = 0; i < 1000000; i++){
        _doSomethingSynchronously();
    }
}

Cyllene - Multithreading en Flutter

L'Event Loop

  • Les évènement asynchrones sont référencés dans la pile des évènements : Event Queue

      - I/0

      - Gestures

      - Streams

      - ...

      - Futures

  • Lorsqu'elle en a la possibilité, l'Event Loop pioche dans la pile le prochain à traiter 

Cyllene - Multithreading en Flutter

L'Event Loop

L'Event Loop est une boucle infinie qui check régulièrement si une opération I/O doit être exécutée ou est terminée

Cyllene - Multithreading en Flutter

Séquençage du code

  1. Exécution des opérations synchrones

     
  2. Dès que possible, l'Event Loop pioche dans la Queue un évènement asynchrone à gérer

Ce n'est donc pas de la vraie concurrence

Cyllene - Multithreading en Flutter

Penchons-nous sur les Futures

Cyllene - Multithreading en Flutter

Définition

  • Tâche s'executant de manière asynchrone et se terminant (avec succès ou non) dans le futur


  • Equivalent des Promises en Javascript

Cyllene - Multithreading en Flutter





          void main() {
            Future(() {
              return 42;
            }).then((value) {
              print("Value: $value");
            }).catchError((error) {
              print("Error: $error");
            });
          }

Cyllene - Multithreading en Flutter




void main() {
  _getIntFromNetwork().then((int value) {
    print("Got value: $value");
  }).catchError((error) {
    print("Got an error: $error");
  });
}

Future<int> _getIntFromNetwork() async {
  await Future.delayed(Duration(seconds: 2));
  return 42;
}

Cyllene - Multithreading en Flutter

Comment cela fonctionne en interne ?

Cyllene - Multithreading en Flutter

  1. La Future est créée et répertoriée quelque part
     
  2. Le code de la Future est mis dans la Queue des Event
     
  3. L'instance de la Future est retournée avec un statut incomplet
     
  4. Si il y en a, les instructions suivantes sont effectués
     
  5. Lorsqu'elle le peut, l'Event Loop pioche le code de la future et l'execute
     
  6. Après quoi then ou catchError seront appelés

Cyllene - Multithreading en Flutter

Dans quel ordre les messages s'afficheront ?

void main(){
    print('1');
    Future((){
        print('2');
    }).then((_){
        print('3');
    });
    print('4');
}

Cyllene - Multithreading en Flutter

Résultat

1
4
2
3
  1. Premier print
  2. Le code de la Future est ajouté à l'Event Loop
  3. Print 4
  4. L'Event loop pioche le code de la Future
  5. Le then est appelé

Cyllene - Multithreading en Flutter

Ce qu'il faut en retenir :

  • Les Futures ne sont pas effectuées en parallèle !
  • L'ensemble du code respecte toujours le même séquençage, les Futures ne font pas exception

Cyllene - Multithreading en Flutter

void main() {
  print("1");
  Future(() {
    print("2");
  }).then((value) {
    print("3");
  }).catchError((error) {
    print("Error: $error");
  });

  for (int i = 0; i < 10000000; i++) {
    print("ok $i");
  }
}

Ici, la boucle s'effectuera toujours avant les prints 2 et 3

Cyllene - Multithreading en Flutter

Le cas async / await

Cyllene - Multithreading en Flutter

  • Une fonction avec le mot clé async retourne une Future
     
  • Le code de la fonction est effectué de manière synchrone jusqu'au premier await
     
  • La méthode attend ensuite que la Future soit terminée pour reprendre son exécution

Cyllene - Multithreading en Flutter

void main() {
  print("1");
  Future(() {
    print("2");
  }).then((_) {
    print("3");
  });
}
void main() async {
  print("1");
  
  await Future(() {
    print("2");
  });
  
  print("3");
}

Ces deux codes sont strictement équivalents :

Cyllene - Multithreading en Flutter

async / await ne change RIEN au séquençage habituel et à l'Event Loop  

Ce qu'il faut en retenir

Cyllene - Multithreading en Flutter

Quelques exemples

Qu'affichent ces différents programmes ?

Pourquoi ?

Cyllene - Multithreading en Flutter




void main() async {
  await a();
  await b();
}

Future<void> a() async {
  await Future.delayed(Duration(seconds: 1));
  print("A");
}

Future<void> b() async {
  print("B start");
  await c();
  print("B end");
}

Future<void> c() async {
  print("C start");

  Future(() => print("C Middle")).then((_) => print("C Middle End"));

  print("C end");
}

Cyllene - Multithreading en Flutter


void main() {
  final List<String> list = ["Flutter", "is", "awesome"];
  a(list);
  b(list);
}

void a(List<String> list) async {
  print("Before loop A");
  list.forEach((String value) async {
    await delayedPrint(value);
  });
  print("After loop A");
}

void b(List<String> list) async {
  print("Before loop B");
  for (int index = 0; index < list.length; index++) {
    await delayedPrint(list[index]);
  }
  print("After loop B");
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print(value);
}

Cyllene - Multithreading en Flutter

a() b()
Before loop A Before loop B
After loop Flutter (après 1 seconde)
Flutter (après 1 seconde) is (après 1 seconde)
is (tout de suite après) awesome (après une seconde)
awesome (tout de suite après) After loop B (tout de suite après)

Cyllene - Multithreading en Flutter

Pourquoi ce résultat différent avec a()?

  • forEach() prend une callback en paramètre
     
  • Celle-ci est notée async et est donc une Future
     
  • La callback est exécutée jusqu'au await puis tout le reste est mis dans la Queue des Events
     
  • L'execution suit son cours et "After loop A" est appelé
     
  • Enfin l'Event Loop dépile toutes les callbacks d'un coup

Cyllene - Multithreading en Flutter

Petit bonus : Comment utiliser forEach quand même ?





void a(List<String> list) async {
  print("Before loop A");
  await Future.forEach(list, (String el) async {
    await delayedPrint(el);
  });
  print("After loop A");
}

Cyllene - Multithreading en Flutter

Le vrai Multi-Threading : Les Isolates

Cyllene - Multithreading en Flutter

Les Isolates

  • Un seul Isolate créé au lancement de l'application
     
  • Il a sa propre zone mémoire
     
  • Il a sa propre Event Loop

Cyllene - Multithreading en Flutter

Nous pouvons créer d'autres Isolates

Cyllene - Multithreading en Flutter

Chaque isolate aura :

  • Sa propre mémoire
     
  • Sa propre Event Loop

Cyllene - Multithreading en Flutter

Comparons avec les Threads habituels :

Avantages :

  • Pas de gestion de mémoire partagée (locks, etc.)
     
  • Du vrai parallelisme 

Inconvénients :

  • Le partage d'informations entre Isolates est un peu plus "compliqué"

Cyllene - Multithreading en Flutter

Comment les utiliser ?

Cyllene - Multithreading en Flutter

Prenons cette application :

Cyllene - Multithreading en Flutter

Les boutons vont tous les deux lancer ce "gros" calcul :

int bigTask(num n) {
  int result = 0;
  for (int i = 0; i < n; i++) {
    result += i;
  }

  return result;
}

Cyllene - Multithreading en Flutter

Tout d'abord, en utilisant une Future :

  _runOnMainThread(BuildContext context) {
    Future<int>(() {
      return bigTask(NB_ITERATIONS);
    }).then((int value) {
      _showSnackBar(context, value);
    });
  }

Cyllene - Multithreading en Flutter

Résultat (pour 1 milliard d'itérations) : 

Cyllene - Multithreading en Flutter

Utilisons un Isolate séparé !

Transformer notre code pour utiliser un Isolate va-t-il être un processus :

  • Extrêmement long
  • Extrêmement pénible
  • Nécessitant une refacto de plusieurs jours et donc en fait on va garder un UI un peu crade parce qu'on a pas le temps pour ce sprint mais on se garde ça de côté pour le prochain, promis

L'UI freeze car la Future n'est pas réellement jouée en parallèle...

?

Cyllene - Multithreading en Flutter

NON

Cyllene - Multithreading en Flutter

Nous allons utiliser la manière la plus simple : 

 

La méthode compute provenant de flutter/foundation.dart

 

Voici la méthode utilisant un Isolate séparé :

_runOnSecondIsolate(BuildContext context) async {
  final int result = await compute(bigTask, NB_ITERATIONS);

  _showSnackBar(context, result);
}

Cyllene - Multithreading en Flutter

Attention, petit détail :

 

  • Les méthodes lancées via compute doivent être static ou en dehors d'une instance de classe

Donc : 

static int bigTask(num n) {
  int result = 0;
  for (int i = 0; i < n; i++) {
    result += i;
  }

  return result;
}

Cyllene - Multithreading en Flutter

C'est tout !

Cyllene - Multithreading en Flutter

Cyllene - Multithreading en Flutter

Résultat (pour 1 milliard d'itérations) : 

La méthode compute nous a permis de :

  • Créer et lancer un Isolate automatiquement
  • Faire tourner notre code dans cet Isolate
  • Attendre le retour pour lancer notre snackbar

Finalement nous avons : 

  • moins de code à écrire
  • une UI qui ne freeze pas

     
  • du vrai parallélisme !

Cyllene - Multithreading en Flutter

Petite précision :

La méthode compute peut prendre "du temps" à créer et lancer l'Isolate
 

Il arrive donc que ce ne soit pas la solution optimale

Cyllene - Multithreading en Flutter

Alors comment choisir ?

Selon le temps que prendra à priori votre tâche :

  •  quelques millisecondes ?
    Les Futures sont un bon choix
     
  • plusieurs centaines de millisecondes ?
    Partez pour un Isolate !

Cyllene - Multithreading en Flutter

Pour aller plus loin :

Nous avons utilisé la manière la plus haut niveau et simple pour créer et utiliser un Isolate

Il est bien sûr possible d'avoir plus de contrôle sur l'Isolate et s'échanger des messages entre Main Isolate et Isolates tiers 

Cyllene - Multithreading en Flutter

Isolate isolate;

void start() async {
  final ReceivePort receivePort = ReceivePort();
  isolate = await Isolate.spawn(task, receivePort.sendPort);
  receivePort.listen((data) {
    print('RECEIVE: ' + data + ', ');
  });
}

void task(SendPort sendPort) {
  int counter = 0;
  Timer.periodic(Duration(seconds: 1), (Timer t) {
    counter++;
    sendPort.send("Counter value = $counter");
  });
}

void stop() {
  if (isolate != null) {
    isolate.kill(priority: Isolate.immediate);
    isolate = null;
  }
}

void main() async {
  await start();
  await stdin.first;
  stop();
  print("Stopped processus");
  exit(0);
}

Cyllene - Multithreading en Flutter

Conclusion

Cyllene - Multithreading en Flutter

  • Comprendre le fonctionnement de l'Event Loop
     
  • Comprendre le fonctionnement des Futures
     
  • Savoir utiliser les Isolates lorsque nécessaire

Pour une bonne UI et donc une bonne application, il est primordial de :

Cyllene - Multithreading en Flutter

Remerciements et sources

Cyllene - Multithreading en Flutter

Merci

Cyllene - Multithreading en Flutter

Avez-vous des questions ?

Cyllene - Multithreading en Flutter

A suivre : 

  • Code promo Flutter Europe

  • Un dernier petit challenge ...

Cyllene - Multithreading en Flutter

InteractParis241

Un dernier petit challenge ?

Cyllene - Multithreading en Flutter

Dans quel ordre les messages s'afficheront ?

void main() async {
  print("1");
  await a();
  print("7");
}

Future<void> a() async {
  print("2");

  Future(() => print("3")).then((_) {
    throw Exception();
    print("4");
  }).catchError((error) {
    print("5");
  });

  print("6");
}

Cyllene - Multithreading en Flutter

Parlons Multithreading en Flutter

By Ecalle Thomas

Parlons Multithreading en Flutter

  • 336