Fermer

février 25, 2023

Isolats dans Flutter | AU NOUVEAU Blog

Isolats dans Flutter |  AU NOUVEAU Blog


Je me suis toujours demandé quel ensemble de code exécuter en arrière-plan pour rendre mon application puissante et réactive, mais je ne sais pas vraiment comment. Il y a quelque temps, j’ai découvert les isolats et j’ai essayé de les mettre en œuvre. Et je dois vous dire que c’était douloureux. Mais j’ai récemment découvert à quel point c’est devenu facile. Alors voilà.

Vous avez peut-être entendu parler des isolats, mais vous n’avez jamais vraiment compris. Ou peut-être avez-vous implémenté des isolats, mais le code a toujours été compliqué et fastidieux à écrire. Dans tous les cas, ce blog vous guidera à travers les hauts et les bas de l’histoire d’Isolate et de la mise en œuvre actuelle et meilleure. Vous voudrez peut-être utiliser la dernière méthode ou vous voudrez peut-être utiliser l’ancienne méthode après tout, selon votre cas d’utilisation.

Les bases

C’est ainsi Documentation flottante définit Isolats :

Un contexte d’exécution Dart isolé.

Avez-vous compris quelque chose? Au moins je ne l’ai pas fait. Commençons donc à comprendre les isolats, puis nous écrirons notre propre définition. Alors, de quoi avons-nous besoin pour la recette des isolats ?

  1. Que sont les isolats ?
  2. Pourquoi avons-nous besoin d’eux?
  3. Qu’est-ce que la gestion des événements ?
  4. Comment implémenter Isolats ?
  5. Et enfin, que sont les groupes isolés ?

Nous allons commencer par la vue d’ensemble des isolats et voir ce que cela signifie vraiment et aller en profondeur et assembler toutes les parties pour voir comment chaque partie fonctionne ensemble afin que nous puissions comprendre ce que font vraiment les isolats et pourquoi avons-nous besoin eux.

Que sont les isolats ?

Pour bien comprendre les isolats, il faut d’abord remonter plus loin et s’assurer de connaître la réponse à ces 2 questions :

  1. Quelle est la différence entre les cœurs de processeur et les threads ?
    Le noyau est un composant matériel physique, tandis que le thread est le composant virtuel qui gère les tâches du noyau. Les cœurs permettent d’effectuer plus de travail simultanément, tandis que les threads améliorent la vitesse et le débit de calcul. Les cœurs utilisent la commutation de contenu, mais les threads utilisent plusieurs processeurs pour exécuter différents processus.
  2. Quelle est la différence entre le traitement simultané et le traitement parallèle ?
    La simultanéité se produit lorsque deux tâches ou plus peuvent démarrer, s’exécuter et se terminer dans des périodes qui se chevauchent. Cela ne signifie pas nécessairement qu’ils fonctionneront tous les deux au même instant. Par exemple, le multitâche sur une machine monocœur. Le parallélisme se produit lorsque des tâches s’exécutent littéralement en même temps, par exemple sur un processeur multicœur.

Revenons aux Isolats.

Dart utilise le modèle Isolate pour la simultanéité. L’isolat n’est rien d’autre qu’un emballage autour du fil. Mais les threads, par définition, peuvent partager de la mémoire, ce qui peut être facile pour le développeur, mais rend le code sujet aux conditions de concurrence et aux verrous. Les isolats, quant à eux, ne peuvent pas partager de mémoire et s’appuient plutôt sur un mécanisme de transmission de messages pour communiquer entre eux. Si quelque chose est difficile à comprendre, continuez à lire. Je suis sûr que vous l’obtiendrez.

À l’aide d’isolats, le code Dart peut effectuer plusieurs tâches indépendantes à la fois, en utilisant des cœurs supplémentaires s’ils sont disponibles. Chaque isolat a sa propre mémoire et un seul thread exécutant une boucle d’événements. Nous arriverons à la boucle d’événements dans une minute.

Pourquoi avons-nous besoin d’isolats ?

Avant d’entrer dans les détails, nous devons d’abord comprendre comment fonctionne réellement l’attente asynchrone.

Nous voulons lire certaines données d’un fichier, puis décoder ce JSON et imprimer la longueur des clés JSON. Nous n’avons pas besoin d’entrer dans les détails de la mise en œuvre ici, mais nous pouvons nous aider de l’image ci-dessous pour comprendre comment cela fonctionne.

Lorsque nous cliquons sur ce bouton, Enchérir, il envoie une requête à _readFileAsync, qui est le code de fléchette que nous avons écrit. Mais cette fonction _readFileAsync, exécute le code en utilisant Dart Virtual Machine/OS pour effectuer l’opération d’E/S, qui en soi est un thread différent, le thread d’E/S. Cela signifie que le code de la fonction principale s’exécute à l’intérieur de l’isolat principal. Lorsque le code atteint le _readFileAsync, il transfère l’exécution du code au thread d’E/S et l’isolat principal attend que le code soit complètement exécuté ou qu’une erreur se produise. C’est ce que fait le mot clé await.

Maintenant, une fois que le contenu des fichiers est lu, le contrôle revient à l’isolat principal, et nous commençons à analyser les données String en tant que JSON et imprimons le nombre de clés. C’est assez simple. Mais supposons que l’analyse JSON était une très grosse opération, considérant un JSON très énorme et nous commençons à manipuler les données pour se conformer à nos besoins. Ensuite, ce travail se passe sur l’isolat principal. À ce stade, l’interface utilisateur pourrait se bloquer, ce qui rendrait nos utilisateurs frustrés.

Qu’est-ce que la gestion des événements ?

Comme nous en avons discuté, Isolate est un wrapper autour d’un thread, et chaque Isolate a une boucle d’événement exécutant des événements. Ces événements ne sont rien d’autre que ce qui se passe lorsque nous utilisons l’application. Ces événements sont ajoutés dans une file d’attente que la boucle d’événement prend et traite ensuite. Ces événements sont traités selon le mode premier entré, premier sorti. Cette image ci-dessous n’est qu’un exemple.

Boucle d'événement

Utilisons à nouveau ce code pour comprendre les gestionnaires d’événements. Nous savons déjà ce qui se passe dans ce bloc.

Nos applications démarrent et dessinent l’interface utilisateur (Paint Event) qui est poussée dans la file d’attente. Nous cliquons sur le Enchérir le bouton et le code de gestion des fichiers démarrent. Ainsi, l’événement Tap est poussé dans la file d’attente. Une fois terminé, supposons que l’interface utilisateur soit mise à jour, donc à nouveau l’événement Paint est poussé dans la file d’attente.

Maintenant, parce que notre logique de gestion du fichier et de JSON était très petite, l’interface utilisateur ne bégaie pas et ne se bloque pas. Mais imaginons à nouveau, pendant un moment, que le code de gestion des fichiers était énorme et que cela prenait beaucoup de temps. Maintenant, la file d’attente d’événements et la boucle d’événements ressemblent à cette image ci-dessous.

Événement Jank

Maintenant que l’isolat principal prend beaucoup de temps pour traiter cet événement, notre animation ou notre interface utilisateur peut se bloquer et irriter vos utilisateurs, provoquant d’énormes pertes. C’est là qu’intervient la création d’un nouvel isolat ou d’un isolat de travailleur.

Comment implémenter Isolats ?

Tout notre code de fléchettes dans l’application Flutter s’exécute en isolat. Qu’il s’agisse d’un isolat principal ou d’un isolat de travailleur, c’est à vous de décider. L’isolat principal est créé pour vous et vous n’avez rien d’autre à faire ici. La fonction principale démarre sur l’isolat principal. Une fois que notre fonction principale est en cours d’exécution, nous pouvons commencer à générer de nouveaux isolats.

Il y a donc 2 façons de mettre en œuvre les isolats, la méthode courte et nouvelle ou la méthode longue et ancienne. Nous pouvons utiliser l’un ou l’autre selon le cas d’utilisation.

Commençons par la méthode déjà existante.

Comme nous en avions déjà discuté, les isolats, contrairement aux threads, ne partagent pas de mémoire. Ceci est fait afin d’éviter les conditions de course et les blocages. Mais la communication entre les isolats se fait par transmission de messages. Ces messages sont des primitives et vous pouvez consulter la liste complète des objets qui peuvent être transmis entre les isolats ici.

Pour faire passer des messages, Dart nous fournit des Ports. SendPort et ReceivePort.

Puisque nous discutons de l’ancienne méthode pour générer des isolats, nous devons savoir que les méthodes d’isolement doivent être des fonctions de niveau supérieur ou statiques.

Voici la lien au code si vous voulez suivre.

Que fait ce code:

  1. Ici, nous créons une instance de ReceivePort pour recevoir des données. N’oubliez pas qu’il s’agit de l’ancienne méthode pour générer des isolats. Cela peut être un peu long mais il faut connaître les détails.
  2. Nous créons un isolat de travailleur sur l’isolat principal à l’aide d’Isolate.spawn et transmettons une fonction de niveau supérieur qui exécute le code de blocage. Nous transmettons également une liste d’arguments, le premier, SendPort qui sera utilisé pour envoyer les données du travailleur Isolate, et le second est le lien de téléchargement. Nous attendons que le nouvel Isolate soit créé.
  3. Nous attendons ensuite le résultat, qui est une forme de chaîne et l’utilisons comme nous le voulons. Ces données peuvent être n’importe quoi de ce liste d’objet.
  4. ResultPort.first utilise un abonnement de flux derrière l’écran et attend que les données de l’isolat de travail y soient poussées. Dès que le premier article arrive, nous renvoyons le résultat.

Il s’agit de la fonction _readAndParseJson qui reçoit l’argument et exécute le code d’isolement du travailleur. Il s’agit d’une fonction factice qui ne fait que retarder la commande de 2 secondes puis se termine. La fonction exit termine l’isolement en cours de manière synchrone. Certaines vérifications sont effectuées avant de renvoyer les données à l’isolat appelant et les données sont renvoyées à l’aide du SendPort.

Bien que cela fonctionne correctement, nous n’avons pas géré les erreurs pouvant être générées par l’isolat de travail ou toute erreur pouvant survenir lors de la génération d’un nouvel isolat.

Tout est à peu près pareil ici, nous avons juste ajouté la gestion des erreurs ici.

Ce que fait ce code est :

  1. Nous ajoutons errorsAreFatal à true lors de la génération d’un nouvel isolat pour nous assurer que l’isolat principal est au courant de toute erreur. Nous attribuons les gestionnaires SendPort pour onExit et onError pour nous assurer que des erreurs se produisent lors de la sortie ou de la génération.
  2. Nous ajoutons également un bloc try-catch lors de la génération d’un nouvel isolat pour nous assurer que si une erreur se produit lors de la génération, nous l’attrapons et arrêtons complètement cette opération.
  3. Si le frai est réussi et que certaines données proviennent de l’isolat du travailleur, nous devons vérifier s’il s’agit d’une erreur ou non.
  4. Si le message renvoyé est nul, cela signifie que l’isolat est sorti sans aucun message et qu’une erreur s’est produite. Si la réponse est une liste, cela signifie que le travailleur Isolate a renvoyé une erreur et un stacktrace. Sinon, c’est une transaction réussie.

Cela semble exagéré si nous voulions faire passer un message unique. Un message et fermez l’isolat. Chaque fois que vous vouliez générer un nouvel isolat, vous devrez réécrire le même code. Étant donné que la logique Isoler est assez personnalisée. Chaque fois, vous voudrez peut-être faire passer des arguments différents et ce serait très fastidieux. C’est pourquoi une nouvelle méthode a été imaginée pour les opérations ponctuelles.

La nouvelle méthode : Isolate.run

C’est tout ce qu’il y a pour la nouvelle méthode.

Nous générons un nouvel Isolate en utilisant la méthode run qui résume tous les détails granulaires et la gestion des erreurs et vous fait gagner beaucoup de temps. Cela aide à générer, à gérer les erreurs, à transmettre des messages et à terminer l’Isolate en utilisant ces quelques petites lignes de code.

Une chose à noter ici est que la fonction _readAndParseJsonWithoutIsolateLogic ne contient aucune logique personnalisée pour Isolate. Pas de ports, pas d’arguments.

Quand utiliser la nouvelle méthode Run et quand utiliser l’ancienne méthode spawn ?

Ces exemples ci-dessus montrent la transmission de messages qui ne se produit qu’une seule fois. La méthode run doit donc être utilisée. Cela réduit considérablement les lignes de code et les cas de test.

Mais si vous voulez créer quelque chose qui nécessite la transmission de plusieurs messages entre les isolats, nous devons utiliser l’ancien Isoler.spawn() méthode. Par exemple, lorsque vous commencez à télécharger un fichier sur un isolat de travail et que vous souhaitez afficher la progression du téléchargement sur l’interface utilisateur. Cela signifie que le compte de progression doit être répété encore et encore.

Avec cela, nous devons implémenter l’ensemble SendPort et ReceivePort pour le passage des messages et la logique personnalisée pour recevoir les arguments et renvoyer la progression à l’Isolate principal.

Que sont les groupes d’isolement ?

Donc, nous savons déjà comment Isolates se transmet des messages. Mais supposons que le message que nous transmettons est un énorme JSON. Avant Dart 2.15, cet énorme objet passant, pouvait impliquer un bégaiement dans l’interface utilisateur. En effet, nous savons déjà qu’Isolate a une certaine mémoire, et lorsqu’un Isolate passe un objet à l’autre, cet objet doit être copié en profondeur. Cela signifiait beaucoup de temps pour copier l’objet dans l’isolat principal, ce qui peut provoquer un jank.

Pour éviter cette circonstance, les isolats ont été retravaillés et les groupes d’isolats ont été inventés. Groupes d’isolats, c’est-à-dire un groupe d’isolats, qui partagent certaines structures de données internes communes représentant l’application en cours d’exécution. Cela signifie qu’à chaque fois qu’un nouvel isolat est généré, de nouvelles structures de données internes n’ont pas besoin d’être à nouveau construites. Parce qu’ils les partagent ensemble.

Ne confondez pas ces structures de données internes avec les objets modifiables. Les Isolats ne peuvent toujours pas partager ce souvenir entre eux. La transmission de messages est toujours nécessaire. Mais, comme les isolats du même groupe d’isolats partagent le même tas, cela signifie que générer un nouvel isolat est 100 fois plus rapide et consomme 10 à 100 fois moins de mémoire.

Un exemple est un isolat de travail qui effectue un appel réseau pour obtenir des données, analyse ces données dans un grand graphique d’objets JSON, puis renvoie ce graphique JSON à l’isolat principal. Avant Dart 2.15, ce résultat devait être copié en profondeur, ce qui pouvait lui-même provoquer un blocage de l’interface utilisateur si la copie prenait plus de temps que le budget de trame. Cela signifie que l’Isolate principal peut recevoir ce JSON en temps quasi constant. Et l’envoi de messages est maintenant environ 8 fois plus rapide.

La bonne nouvelle est que, si vous utilisez une version de Flutter supérieure à 2.8, vous n’avez rien à faire pour utiliser ces avancées.

J’espère que vous avez aimé la compréhension des isolats. Si vous avez des doutes, veuillez commenter.

Référence pour le code : https://github.com/DhruvamSharma/NFT-Material3




Source link