Ivan Tulup: asynchrone en JS sous le capot

Connaissez-vous Ivan Tulup? Très probablement oui, vous ne savez pas encore de quel type de personne il s'agit, et vous devez faire très attention à l'état de son système cardiovasculaire.

À ce sujet et comment l'asynchronisme fonctionne dans JS sous le capot, comment fonctionne la boucle d'événement dans les navigateurs et Node.js, y a-t-il des différences et peut-être des choses similaires ont été racontées par Mikhail Bashurov ( SaitoNakamura ) dans son rapport sur RIT ++. Nous sommes heureux de partager avec vous la transcription de cette présentation informative.



À propos de l'orateur: Mikhail Bashurov est un développeur Web fullstack sur JS et .NET de Luxoft. Il aime la belle interface utilisateur, les tests verts, la transpilation, la compilation, le compilateur permettant la technique et améliorer l'expérience de développement.

Note de l'éditeur: le rapport de Mikhail était accompagné non seulement de diapositives, mais d'un projet de démonstration dans lequel vous pouvez cliquer sur les boutons et regarder indépendamment l'exécution des shuffles. La meilleure option serait d'ouvrir la présentation dans un onglet adjacent et de s'y référer périodiquement, mais le texte fournira également des liens vers des pages spécifiques. Et maintenant, nous passons la parole à l'orateur, profitez-en pour lire.


Grand-père Ivan Tulup


J'ai eu une candidature pour Ivan Tulup.



Mais j'ai décidé d'emprunter une voie plus conformiste, alors rencontrez - le grand-père Ivan Tulup!



En fait, seulement deux choses doivent être connues de lui:

  1. Il aime jouer aux cartes.
  2. Comme tout le monde, il a un cœur et il bat.

Faits sur la crise cardiaque


Vous avez peut-être entendu dire que les cas de maladies cardiaques et leur mortalité sont récemment devenus plus fréquents. La maladie cardiaque la plus courante est probablement une crise cardiaque, c'est-à-dire une crise cardiaque.

Qu'est-ce qui est intéressant avec la crise cardiaque?

  • Le plus souvent, cela se produit le lundi matin.
  • Chez les personnes seules, le risque de crise cardiaque est deux fois plus élevé. Ici, peut-être, le point est uniquement en corrélation, et non dans une relation causale. Malheureusement (ou heureusement), il en est ainsi.
  • Dix chefs d'orchestre sont décédés d'une crise cardiaque pendant la conduite (travail apparemment très nerveux!).
  • Une crise cardiaque est une nécrose du muscle cardiaque causée par un manque de circulation sanguine.

Nous avons une artère coronaire qui apporte du sang au muscle (myocarde). Si le sang commence à y circuler mal, le muscle meurt progressivement. Naturellement, cela a un effet extrêmement négatif sur le cœur et son travail.

Le grand-père Ivan Tulup a aussi un cœur et il bat. Mais notre cœur pompe le sang, et le cœur d'Ivan Tulup pompe notre code et nos peines.

Tasky: un grand cercle de circulation sanguine


Quelles sont les tâches? Qu'est-ce qui peut être généralement paresseux dans un navigateur? Pourquoi sont-ils nécessaires du tout?

Par exemple, nous exécutons du code à partir d'un script. C'est un battement de cœur, et maintenant nous avons le flux sanguin. Nous avons cliqué sur le bouton et nous sommes abonnés à l'événement - le gestionnaire d'événements pour cet événement a craché - le rappel que nous avons envoyé. Ils ont mis Timeout, Callback travaillé - une autre tâche. Et donc dans certaines parties, un battement de cœur est une tâche.



Il existe de nombreuses sources différentes de chou, selon les spécifications, il y en a un grand nombre. Notre cœur continue de battre, et pendant qu'il bat, tout va bien pour nous.

Boucle d'événement dans le navigateur: version simplifiée


Cela peut être représenté dans un diagramme très simple.



  • Il y a une tâche, nous l'avons accomplie.
  • Ensuite, nous exécutons le rendu du navigateur.

Mais en fait, cela n'est pas nécessaire, car dans certains cas, le navigateur peut ne pas afficher entre deux tâches.

Cela peut se produire, par exemple, si le navigateur peut décider de regrouper plusieurs délais d'expiration ou plusieurs événements de défilement. Ou à un moment donné, quelque chose ne va pas, et le navigateur décide au lieu de 60 ips (fréquence d'images normale pour que tout se passe bien et en douceur) d'afficher 30 ips. Ainsi, il aura beaucoup plus de temps pour exécuter votre code et autres travaux utiles, il pourra effectuer plusieurs chocs.

Par conséquent, le rendu n'est pas vraiment effectué après chaque tâche.

Tasky: classification


Il existe deux types d'opérations potentielles:

  1. E / S liées;
  2. Lié au processeur.

Le CPU est notre travail utile que nous faisons (croire, afficher, etc.)

Les E / S liées sont les points auxquels nous pouvons partager nos tâches. Ce pourrait être:

  • Timeout
Nous avons fait setTimeout 5000 ms, et nous attendons juste ces 5000 ms, mais nous pouvons faire d'autres travaux utiles. Ce n'est que lorsque ce temps passe, que nous recevons un rappel et que nous y travaillons.

  • xhr / fetch.
Nous sommes allés en ligne. Pendant que nous attendons une réponse du réseau, nous attendons simplement, mais nous pouvons aussi faire quelque chose d'utile.

  • Réseau (OBD).
Ou, par exemple, nous allons sur Network BD. Nous parlons également de Node.js, y compris, et si nous voulons aller quelque part vers le réseau à partir de Node.js, veuillez - c'est la même tâche potentielle liée aux E / S (entrée / sortie).

  • Fichier.
Lisez le fichier - ce n'est potentiellement pas une tâche liée au processeur. Dans Node.js, il s'exécute dans le pool de threads en raison d'une API Linux légèrement tordue, pour être honnête.

Alors CPUbound est:

  • Par exemple, lorsque nous faisons une boucle for of / for (;;) ou que nous traversons le tableau d'une manière ou d'une autre en utilisant des méthodes supplémentaires: filtre, carte, etc.
  • JSON.parse ou JSON.stringify, c'est-à-dire la sérialisation / désérialisation des messages. Tout cela se fait sur le CPU, nous ne pouvons pas attendre que tout soit exécuté par magie quelque part.
  • Compter les hachages, c'est-à-dire, par exemple, l'extraction de crypto.

Bien sûr, la crypto peut également être exploitée sur le GPU, mais je pense - GPU, CPU - vous comprenez cette analogie.

Tasky: arythmie et thrombus


En conséquence, il s'avère que notre cœur bat: il fait une tâche, la deuxième, la troisième - jusqu'à ce que nous fassions quelque chose de mal. Par exemple, nous parcourons un tableau de 1 million d'éléments et comptons la somme. Il semblerait que ce ne soit pas si difficile, mais cela peut prendre du temps tangible. Si nous prenons constamment du temps tangible sans libérer de tâche, notre rendu ne peut pas être effectué. Il a plané dans ce désir, et tout - l'arythmie commence.

Je pense que tout le monde comprend que l'arythmie est une maladie cardiaque plutôt désagréable. Mais vous pouvez toujours vivre avec lui. Que se passe-t-il si vous placez une tâche qui suspend simplement la boucle d'événement entière dans une boucle sans fin? Vous mettez en quelque sorte un caillot de sang dans la coronaire ou une autre artère, et tout deviendra complètement triste. Malheureusement, notre grand-père Ivan Tulup va mourir.

Alors le grand-père Ivan est mort ...




Pour nous, cela signifie que l'onglet entier se bloque complètement - vous ne pouvez pas cliquer sur quoi que ce soit, puis Chrome dit: "Aw, Snap!"

C'est encore pire que les bugs du site Web en cas de problème. Mais si tout a raccroché, et même, probablement, le processeur chargé et l'utilisateur généralement bloqué, il ne reviendra probablement jamais sur votre site.

Par conséquent, l'idée est la suivante: nous avons une tâche et nous n'avons pas besoin de nous accrocher à cette tâche pendant très longtemps. Nous devons le libérer rapidement, afin que le navigateur, le cas échéant, puisse effectuer un rendu (s'il le souhaite). Si vous ne voulez pas - super, dansez!

Démo de Philip Roberts: Loupe de Philip Roberts


Prenons un exemple :

$.on('button', 'click', function onClick(){ console.log('click'); }); setTimeout(function timeout() { console log("timeout"); }. 5000); console.log(“Hello world"); 

L'essence est la suivante: nous avons un bouton, nous nous y abonnons (addEventListener), Timeout est appelé pendant 5 secondes et immédiatement dans le console.log nous écrivons "Bonjour, monde!", Dans setTimeout nous écrivons Timeout, dans onClick nous écrivons Click.

Que se passera-t-il si nous l'exécutons et plusieurs fois nous cliquons sur le bouton - quand le Timeout sera-t-il réellement exécuté? Voyons la démo:


Le code commence à s'exécuter, se met sur la pile, le Timeout disparaît. Pendant ce temps, nous avons cliqué sur le bouton. Au bas de la file d'attente, plusieurs événements ont été ajoutés. Pendant que Click est en cours d'exécution, Timeout attend, bien que 5 secondes se soient écoulées.

Ici, onClick est rapide, mais si vous mettez une tâche plus longue, alors tout se fige, comme déjà expliqué. Ceci est un exemple très simplifié. Voici un tour, mais dans les navigateurs, en fait, tout n'est pas le cas.

Dans quel ordre les événements sont-ils exécutés - que dit la spécification HTML?

Elle dit ce qui suit: nous avons 2 concepts:

  1. source de la tâche;
  2. file d'attente des tâches.

La source de tâche est une sorte de tâche. Cela peut être une interaction utilisateur, c'est-à-dire onClick, onChange - quelque chose avec lequel l'utilisateur interagit; ou des minuteries, c'est-à-dire setTimeout et setInterval, ou PostMessages; ou même des types complètement sauvages comme la source de tâche Canvas Blob Serialization - également un type distinct.

La spécification indique que pour la même tâche, les tâches seront garanties d'être exécutées dans l'ordre où elles sont ajoutées. Pour tout le reste, rien n'est garanti, car il peut y avoir un nombre illimité de files d'attente de tâches. Le navigateur décide combien il y en aura. À l'aide de la file d'attente des tâches et de leur création, le navigateur peut hiérarchiser certaines tâches.

Priorités du navigateur et files d'attente de tâches




Imaginez que nous ayons 3 lignes:

  1. interaction avec l'utilisateur;
  2. délais
  3. publier des messages.

Le navigateur commence à obtenir des tâches à partir de ces files d'attente:

  • Tout d'abord, il prend en charge l'interaction utilisateur - c'est très important - un battement de cœur a disparu.
  • Puis il prend postMessages - eh bien, postMessages est une priorité assez élevée, cool!
  • Le suivant, onChange, est également à nouveau de l'interaction utilisateur en priorité.
  • Le prochain onClick est envoyé. La file d'attente d'interaction avec l'utilisateur est terminée, nous avons montré à l'utilisateur tout ce dont il avait besoin.
  • Ensuite, nous prenons setInterval , ajoutons postMessages.
  • setTimeout n'exécutera que la plus récente . Il était quelque part au bout de la ligne.

Il s'agit là encore d'un exemple très simplifié et, malheureusement, personne ne peut garantir comment cela fonctionnera dans les navigateurs , car ils décident eux-mêmes de tout cela. Vous devez tester cela vous-même si vous voulez savoir ce que c'est.

Par exemple, postMessages a priorité sur setTimeout. Vous avez peut-être entendu parler d'une chose telle que setImmediate, qui, par exemple, dans les navigateurs IE, n'était que native. Mais il existe des polyfichiers qui sont principalement basés non pas sur setTimeout, mais sur la création d'un canal postMessages et sur son abonnement. Cela fonctionne généralement plus rapidement car les navigateurs le priorisent.

Eh bien, ces tâches sont effectuées. À quel moment terminons-nous notre tâche et comprenons-nous que nous pouvons prendre la suivante, ou que nous pouvons rendre?

Pile


La pile est une structure de données simple qui fonctionne sur le principe du "dernier entré - premier sorti", c'est-à-dire "J'ai mis le dernier - tu as le premier . " La contrepartie la plus proche, probablement réelle, est un jeu de cartes. Par conséquent, notre grand-père Ivan Tulup aime jouer aux cartes.



L'exemple ci-dessus, dans lequel il y a du code, le même exemple peut être poussé dans la présentation . À un certain endroit, nous appelons handleClick, entrez console.log, appelons showPopup et window. confirmer. Formons une pile.

  • Donc, nous prenons d'abord handleClick et poussons l'appel à cette fonction sur la pile - super!
  • Ensuite, nous entrons dans son corps et l'exécutons.
  • Nous mettons console.log sur la pile et l'exécutons immédiatement, car tout est là pour l'exécuter.
  • Ensuite, nous mettons showConfirm - c'est un appel de fonction - super.
  • Nous mettons des fonctions sur la pile - nous mettons son corps, c'est-à-dire window.confirm.

Nous n'avons plus rien - nous le faisons. Une fenêtre apparaîtra: «Êtes-vous sûr?», Cliquez sur «Oui», et tout quittera la pile. Nous avons maintenant terminé le corps showConfirm et le corps handleClick. Notre pile est effacée et nous pouvons passer à la tâche suivante. Question: OK, je sais maintenant que vous devez tout casser en petits morceaux. Comment puis-je, par exemple, faire cela dans le cas le plus élémentaire?

Partitionner un tableau en morceaux et les traiter de manière asynchrone


Regardons l'exemple le plus «frontal». Je vous préviens tout de suite: n'essayez pas de répéter cela à la maison - cela ne se compilera pas.



Nous avons un grand, grand tableau, et nous voulons calculer quelque chose en fonction de celui-ci, par exemple, pour analyser certaines données binaires. Nous pouvons simplement le diviser en morceaux: traiter cette pièce, ceci et cela. Nous sélectionnons la taille du morceau, par exemple, 10 000 éléments, nous considérons combien de morceaux nous aurons. Nous avons une fonction parseData qui va dans le CPU et peut vraiment faire quelque chose de lourd. Ensuite, nous divisons le tableau en morceaux, définissez setTimeout (() => parseData (tranche), 0).

Dans ce cas, le navigateur pourra à nouveau hiérarchiser l'interaction de l'utilisateur et effectuer le rendu entre les deux. Autrement dit, vous libérez au moins votre boucle d'événement, et cela continue de fonctionner. Votre cœur continue de battre et c'est bien.

Mais c'est vraiment un exemple très «frontal». Il existe de nombreuses API dans les navigateurs pour vous aider à le faire de manière plus spécialisée.

Outre setTimeout et setInterval, il existe des API qui vont au-delà des limites, telles que, par exemple, requestAnimationFrame et requestIdleCallback.

Beaucoup connaissent probablement requestAnimationFrame et l'utilisent déjà. Il est exécuté avant le rendu. Son charme est que, d'une part, il essaie d'exécuter toutes les 60 ips (ou 30 ips), et d'autre part, tout cela se fait immédiatement avant de créer le modèle d'objet CSS, etc.



Par conséquent, même si vous avez plusieurs requestAnimationFrame, ils regrouperont en fait toutes les modifications et le cadre sortira complet. Dans le cas de setTimeout, vous ne pouvez certainement pas obtenir une telle garantie. Un setTimeout changera une chose, l'autre une autre, et entre le rendu peut glisser - vous aurez une secousse de l'écran ou autre chose. RequestAnimationFrame est idéal pour cela.

En plus de cela, il existe également requestIdleCallback. Vous avez peut-être entendu dire qu'il est utilisé dans React v16.0 (Fibre). RequestIdleCallback fonctionne de telle manière que si le navigateur comprend qu'il a du temps entre les images (60 ips) pour faire quelque chose d'utile, et en même temps, il a déjà tout fait - ils ont fait la tâche, requestAnimationFrame a fait - cela semble cool, alors ça peut produire de petits quanta, disons, 50 ms chacun, vous pouvez donc faire quelque chose (mode IDLE).

Ce n'est pas dans le schéma ci-dessus, car il n'est situé à aucun endroit particulier. Le navigateur peut décider de le placer avant le cadre, après le cadre, entre le requestAnimationFrame et le rendu, après la tâche, avant la tâche. Personne ne peut garantir cela.

Il vous est garanti que si vous avez du travail qui n'est pas lié à la modification du DOM (car alors requestAnimationFrame est une animation et ainsi de suite), alors qu'il n'est pas super prioritaire, mais tangible, alors requestIdleCallback est votre chemin.

Donc, si nous avons une longue opération liée au processeur, nous pouvons essayer de la diviser en morceaux.

  • S'il s'agit d'un changement DOM, utilisez requestAnimationFrame.
  • S'il s'agit d'une tâche non prioritaire, de courte durée et non difficile qui ne surchargera pas le processeur, alors requestIdleCallback.
  • Si nous avons une grosse tâche puissante qui doit être effectuée en permanence, nous allons au-delà de la boucle d'événement et utilisons WebWorkers. Il n'y a pas d'autre moyen.

Tâches dans les navigateurs:

  1. Écrasez tout en petites tâches.
  2. Il existe de nombreux types de tâches.
  3. Les tâches sont hiérarchisées par ces types via des files d'attente de spécifications.
  4. Les navigateurs décident en grande partie et la seule façon de comprendre comment cela fonctionne est de simplement vérifier si l'un ou l'autre code est en cours d'exécution.
  5. Mais le cahier des charges n'est pas toujours respecté!

Le problème est que notre Ivan Tulup est un vieux grand-père, car les implémentations de la boucle d'événement dans les navigateurs sont également très anciennes. Ils ont été créés avant la rédaction de la spécification, de sorte que la spécification est malheureusement respectée dans la mesure où. Même si vous lisez à quoi devrait ressembler la spécification, personne ne garantit que tous les navigateurs la prennent en charge. Assurez-vous donc de vérifier dans les navigateurs comment cela fonctionne réellement.

Le grand-père Ivan Tulup dans les navigateurs est une personne peu prévisible, avec quelques fonctionnalités intéressantes, vous devez vous en souvenir.

Terminator Santa: boucle de mascotte sur Node.js


Node.js ressemble plus à quelqu'un comme ça.



Parce que d'un côté c'est le même grand-père avec une barbe, mais en même temps tout est réparti en phases et il est clairement peint où ce qui se fait.

Phases de la boucle d'événement dans Node.js:

  • minuteries;
  • rappel en attente;
  • au ralenti, se préparer;
  • sondage;
  • vérifier;
  • fermer les rappels.

Tout sauf le dernier n'est pas très clair ce que cela signifie. Les phases ont des noms si étranges, car sous le capot, comme nous le savons déjà, nous avons Libuv pour gouverner tout le monde:

  • Linux - epoll / POSIX AIO;
  • BSD - kqueue;
  • Windows - IOCP;
  • Solaris - ports d'événements.

Des milliers d'entre eux tous!

De plus, Libuv fournit également la même boucle d'événement. Il n'a pas les spécificités de Node.js, mais il y a des phases, et Node.js les utilise simplement. Mais pour une raison quelconque, elle a pris les noms de là.

Voyons ce que signifie réellement chaque phase.

La phase Timers effectue:


  • Minuteries prêtes pour le rappel;
  • setTimeout et setInterval;
  • Mais PAS setImmediate est une phase différente.

Rappels de phase en attente


Avant cela, la phase de documentation appelait les rappels d'E / S. Plus récemment, cette documentation a été corrigée et elle a cessé de se contredire. Avant cela, à un endroit, il était écrit que les rappels d'E / S étaient exécutés dans cette phase, dans une autre - celle de la phase d'interrogation. Mais maintenant, tout y est écrit sans équivoque et bien, alors lisez la documentation - quelque chose deviendra beaucoup plus compréhensible.

Dans la phase de rappel en attente, les rappels de certaines opérations système (erreur TCP) sont exécutés. Autrement dit, si dans Unix il y a une erreur dans le socket TCP, dans ce cas, il ne veut pas le jeter immédiatement, mais dans le rappel, qui sera exécuté juste dans cette phase. C’est tout ce que nous devons savoir sur elle. Nous n'y sommes pratiquement pas intéressés.

Phase inactive, préparer


Dans cette phase, nous ne pouvons rien faire du tout, nous allons donc l’oublier en principe.



Phase de sondage


C'est la phase la plus intéressante de Node.js car elle fait le principal travail utile:

  • Effectue des rappels d'E / S (pas de phase de rappel en attente!).
  • Attente d'événements d'E / S;
  • C'est cool de faire setImmediate;
  • Pas de minuteries;

À l'avenir, setImmediate s'exécutera dans la prochaine phase de vérification, c'est-à-dire garantie avant les temporisateurs.

Et la phase d'interrogation contrôle également le flux de la boucle d'événements. Par exemple, si nous n'avons pas de temporisateurs, il n'y a pas de setImmediate, c'est-à-dire que personne n'a le temporisateur, setImmediate n'a pas appelé, nous bloquons simplement dans cette phase et attendons l'événement des E / S, si quelque chose nous arrive, s'il y a des rappels si nous nous sommes inscrits pour quelque chose.

Comment un modèle non bloquant est-il implémenté? Par exemple, au même Epoll, nous pouvons nous abonner à un événement - ouvrir une socket et attendre que quelque chose y soit écrit. De plus, le deuxième argument est le timeout, c'est-à-dire nous attendrons Epoll, mais si le délai expire et que l'événement des E / S ne se produit pas, il quittera le délai. Si un événement nous vient du réseau (quelqu'un écrit sur socket), il viendra.

Par conséquent, la phase d'interrogation supprime le tas (le tas est une structure de données qui permet une livraison et une livraison bien triées) du premier rappel, prend son délai d'expiration, écrit dans ce délai et libère tout. Ainsi, même si personne ne nous écrit dans le socket, le délai d'attente fonctionnera, retournera à la phase d'interrogation et le travail se poursuivra.

Il est important de noter que dans la phase de sondage, il y a une limite sur le nombre de rappels à la fois.

Il est triste que ce ne soit pas le cas dans les phases restantes. Si vous ajoutez 10 milliards d'expiration, vous ajoutez 10 milliards d'expiration. Par conséquent, la phase suivante est la phase de vérification.

Phase de vérification


C'est là que setImmediate s'exécute. La phase est belle dans la mesure où setImmediate, appelé dans la phase d'interrogation, est garanti pour s'exécuter plus tôt que le temporisateur. Parce que le chronomètre ne sera activé que sur le tick suivant au tout début, et plus tôt à partir de la phase de sondage. Par conséquent, nous ne pouvons pas avoir peur de la concurrence avec d'autres chronomètres et utiliser cette phase pour les choses que nous ne voulons pas pour une raison quelconque exécuter dans un rappel.

Rappels de fermeture de phase


Cette phase n'exécute pas tous nos rappels de fermeture de socket et d'autres types:

 socket.on('close', …). 

Elle ne les exécute que si cet événement a volé de façon inattendue, par exemple, quelqu'un à l'autre bout du fil a envoyé: "Tout - fermez la prise - allez d'ici, Vasya!" Ensuite, cette phase fonctionnera, car l'événement est inattendu. Mais cela ne nous affecte pas particulièrement.

Traitement asynchrone incorrect des morceaux dans Node.js


Que se passera-t-il si nous mettons le même modèle que nous avons pris dans les navigateurs avec setTimeout sur Node.js - c'est-à-dire que nous divisons le tableau en morceaux, pour chaque morceau que nous faisons setTimeout - 0.

 const bigArray = [1..1_000_000] const chunks = getChunks(bigArray) const parseData = (slice) => // parse binary data for (chunk of chunks) { setTimeout(() => parseData(slice), 0) } 

Pensez-vous que cela pose des problèmes?

J'ai déjà un peu avancé lorsque j'ai dit que si vous ajoutez 10 000 délais d'attente (ou 10 milliards!), Il y aura 10 000 temporisateurs dans la file d'attente, et il les obtiendra et les exécutera - il n'y a aucune protection contre cela: obtenez - exécutez, obtenez - à accomplir et ainsi de suite à l'infini.

Seule la phase d'interrogation, si nous obtenons constamment un événement d'E / S, tout le temps que quelqu'un écrit quelque chose dans le socket afin que nous puissions même exécuter des temporisations et setImmediate, il a une protection limite et dépend du système. Autrement dit, il différera sur différents systèmes d'exploitation.

Malheureusement, d'autres phases, y compris les minuteries et setImmediate, ne disposent pas d'une telle protection. Par conséquent, si vous faites comme dans l'exemple, tout gèlera et n'atteindra pas la phase d'interrogation pendant très longtemps.

Mais pensez-vous que quelque chose changera si nous remplaçons setTimeout (() => parseData (tranche), 0) par setImmediate (() => parseData (tranche))? - Naturellement, non, il n'y a pas non plus de protection sur la phase de contrôle.

Pour résoudre ce problème, vous pouvez appeler un traitement récursif .

 const parseData = (slice) => // parse binary data const recursiveAsyncParseData = (i) => { parseData(getChunk(i)) setImmediate(() => recursiveAsyncParseData(i + 1)) } recursiveAsyncParseData(0) 

L'essentiel est que nous avons pris la fonction parseData et écrit son appel récursif, mais pas seulement nous-mêmes, mais via setImmediate. Lorsque vous appelez cela dans la phase setImmediate, il passe au tick suivant, et non au tick courant. Par conséquent, cela libérera la boucle d'événement, cela ira plus loin dans un cercle. Autrement dit, nous avons recursiveAsyncParseData, où nous passons un certain index, obtenons le morceau par cet index, l'analysons - puis mettons la file d'attente setImmediate avec l'index suivant. Cela arrivera à notre prochain tick et nous pouvons traiter récursivement tout cela.

Certes, le problème est qu'il s'agit toujours d'une sorte de tâche liée au processeur. Peut-être qu'elle va encore peser et prendre du temps dans Event Loop. Il est fort probable que vous souhaitiez que vos Node.js soient purement liés aux E / S.
Par conséquent, il vaut mieux utiliser d'autres choses, par exemple, le processus fork / thread pool.

Maintenant, nous savons à propos de Node.js que:

  • tout est distribué par phases - eh bien, nous le savons clairement;
  • il y a une protection contre une phase de scrutin trop longue, mais pas le reste;
  • des modèles de traitement récursifs peuvent être appliqués afin de ne pas bloquer la boucle d'événement;
  • Mais il est préférable d'utiliser le processus fork, le pool de threads, le processus enfant

Vous devez également être prudent avec le pool de threads, car Node.js démarre les choses là-haut, en particulier la résolution DNS, car pour Linux, pour une raison quelconque, la fonction de résolution DNS n'est pas asynchrone. Par conséquent, il doit être exécuté dans ThreadPool. Sur Windows, heureusement, non. Mais là, vous pouvez lire des fichiers de manière asynchrone. Sous Linux, malheureusement, c'est impossible.

À mon avis, la limite standard est de 4 processus dans ThreadPool. Par conséquent, si vous faites activement quelque chose là-bas, il sera en concurrence avec tout le monde - avec fs et autres. Vous pouvez envisager d'augmenter ThreadPool, mais aussi très soigneusement. Alors lisez quelque chose sur ce sujet.

Microtâche: circulation pulmonaire


Nous avons des tâches dans Node.js et des tâches dans les navigateurs. Vous avez peut-être déjà entendu parler de microtâche. Voyons ce que c'est et comment ils fonctionnent, et commençons par les navigateurs.

Microtâche dans les navigateurs


Pour comprendre le fonctionnement de la microtâche, nous nous tournons vers l'algorithme de boucle d'événements selon la norme whatwg, c'est-à-dire, allons à la spécification et voyons à quoi tout cela ressemble.



Traduisant en langage humain, cela ressemble à ceci:

  • Prenez la tâche gratuite de notre ligne
  • Nous le réalisons
  • Nous effectuons un point de contrôle des microtâches - OK, nous ne savons toujours pas ce que c'est, mais nous nous en souvenons.
  • Nous mettons à jour le rendu (si nécessaire) et revenons à la case départ.



Ils sont effectués à l'endroit indiqué sur le schéma, et dans plusieurs autres endroits, dont nous allons bientôt connaître. Autrement dit, la tâche est terminée, les microtâches sont exécutées.

Sources de microtucks


  • Promesse.alors.

Important - pas Promise lui-même, à savoir Promise.then. Le rappel qui a été placé alors est une microtâche. Si vous avez appelé 10 alors - vous avez 10 microcars, 10 000 alors - 10 000 microcars.

  • Observateur de mutations.
  • Object.observe , qui est obsolète et dont personne n'a besoin.

Combien utilisent l'observateur de mutation?

Je pense que peu utilisent l'observateur de mutation. Très probablement, Promise.then est plus utilisé, c'est pourquoi nous le considérerons dans l'exemple.

Caractéristiques du point de contrôle des microtâches:

  • Nous faisons tout - cela signifie que nous réalisons tous les microtâches que nous avons dans la file d'attente jusqu'à la fin. Nous ne lâchons rien - nous prenons et faisons tout ce qui est, ils devraient être micro, non?
  • Vous pouvez toujours générer de nouvelles microtâches dans le processus, et elles seront exécutées dans le même point de contrôle de microtâches.
  • Ce qui est également important - ils sont exécutés non seulement après l'exécution de la tâche, mais également après avoir effacé la pile.

Ceci est un point intéressant. Il s'avère qu'il est possible de générer de nouveaux microtâches et nous les remplirons tous tous. À quoi cela peut-il nous conduire?


Nous avons deux cœurs. J'ai animé le premier cœur avec l'animation JS, et le second avec l'animation CSS. Il existe une autre fonctionnalité intéressante appelée starveMicrotasks. Nous appelons Promise.resolve, puis mettons la même fonction dans then.
Voyez dans la présentation ce qui se passe si vous appelez cette fonction.

Oui, le cœur de JS s'arrêtera, car nous ajoutons une microtâche, puis nous y ajoutons une microtâche, puis nous y ajoutons une microtâche ... Et ainsi de suite sans fin.

Autrement dit, l'appel récursif de microtucks va tout bloquer. Mais il semblerait que j'ai tout asynchrone! Il faut le laisser partir, j'ai appelé setTimeout là-bas. Non! Malheureusement, vous devez être prudent avec la microtâche, donc si vous utilisez un appel récursif d'une manière ou d'une autre, soyez prudent - vous pouvez tout bloquer.

De plus, comme nous nous en souvenons, la microtâche est exécutée à la fin du nettoyage de la pile. Nous nous souvenons de ce qu'est une pile. Il s'avère que dès que nous sommes sortis de notre code, le rappel setTimeout a été exécuté - c'est tout - les microtâches sont allées là. Cela peut entraîner des effets secondaires intéressants.

Prenons un exemple .



Il y a un bouton et un récipient gris dans lequel il se trouve. Nous souscrivons au clic du bouton et du conteneur. , , , .

2 :

  1. Promise.resolve;
  2. .then, console.log('RO')

«FUS», – «DAH!» ( ).

, ? , , «FUS RO DAH!» Super! , .



, , . – . , - ?



! .



, .

, , , . , .

  • — buttonHandleClick, .
  • Promise.resolve. . , console.log('RO') . .
  • console.log('FUS').
  • buttonHandleClick . .
  • , (divHandleClick) , «DAH!».
  • HandleClick .

, . ? :

  • button.click(). .
  • button HandleClick.
  • Promise.resolve then. , Promise.resolve .
  • console.log «FUS».
  • buttonHandleClick , .

(click) , , . divHandleClick , , console.log('DAH!') . , .

, , button.click .
. , , . , , .

: () ( ). - , , stopPropagation. , , , , - , .

, - ( junior-) — «», promise, , then , - . , , : , , . . , - .

( 4) , . , , , , - . .

, :

  • Event Loop. C'est désagréable.
  • , .

, . — , , .

Node.js


Node.js Promise.then process.nextTick. , — . , , , , .

process.nextTick


, process.nextTick, setImmediate? Node.js ?

. createServer, EventEmitter, , listen ( ), .

 const createServer = () => { const evEmitter = new EventEmitter() return { listen: port => { evEmitter.emit('listening', port) return evEmitter } } } const server = createServer().listen(8080) server.on('listening', () => console.log('listening')) 

, , 8080, listening console.log - .

, , - .

createServer, . listen, , . .

, , . Que peut-on faire? process.nextTick: evEmitter.emit('listening', port) process.nextTick(() => evEmitter.emit('listening', port)).

, process.nextTick , . EventEmitter, . , , API, . process.nextTick, emit , userland . createServer, , listen, listening. — process.nextTick — ! , , .

process.nextTick . , .

, process.nextTick , Promise.then . process.nextTick , — , Event Loop, Node.js. , , .

process.nextTick , ghbvtybnm setImmediate , C++ .. process.nextTick .

Async/await


API — async/await, - . . , async/await Promise, Event Loop . , .

Liens utiles



, !

Frontend Conf — 4 5 , . , :


Venez, ce sera intéressant!

Source: https://habr.com/ru/post/fr417461/


All Articles