web-dev-qa-db-fra.com

Nodejs Event Loop

Existe-t-il en interne deux boucles d'événements dans l'architecture nodejs?

  • libev/libuv 
  • v8 boucle d'événement javascript

Sur une demande d'E/S, le nœud met-il en file d'attente la demande à libeio qui, à son tour, informe de la disponibilité des données via des événements utilisant libev et, enfin, ces événements sont gérés par la boucle d'événements v8 à l'aide de rappels?

En gros, comment libev et libeio sont-ils intégrés à l’architecture de nodejs?

Existe-t-il une documentation disponible pour donner une image claire de l'architecture interne de nodejs?

126
Tamil

J'ai personnellement lu le code source de node.js & v8.

J'ai rencontré un problème similaire à vous, lorsque j'ai essayé de comprendre l'architecture de node.js afin d'écrire des modules natifs.

Ce que je publie ici, c’est ma compréhension de node.js et c’est peut-être un peu hors de propos.

  1. Libev est la boucle d'événement qui s'exécute en interne dans node.js pour effectuer des opérations simples de boucle d'événement. Il a été écrit à l'origine pour les systèmes * nix. Libev fournit une boucle d'événements simple mais optimisée pour l'exécution du processus. Vous pouvez en savoir plus sur libev ici .

  2. LibEio est une bibliothèque permettant d'effectuer une sortie d'entrée de manière asynchrone. Il gère les descripteurs de fichiers, les gestionnaires de données, les sockets, etc. Vous pouvez en savoir plus à ce sujet ici ici .

  3. LibUv est une couche d'abstraction située au sommet de libeio, libev, c-ares (pour DNS) et iocp (pour windows asynchronous-io). LibUv exécute, gère et gère tous les événements io et dans le pool d'événements. (en cas de libeio threadpool). Vous devriez consulter le tutoriel de Ryan Dahl sur libUv. Cela commencera à vous donner plus de sens sur la façon dont libUv fonctionne et vous comprendrez alors comment node.js fonctionne au dessus de libuv et v8.

Pour comprendre uniquement la boucle d'événements javascript, vous devriez envisager de regarder ces vidéos.

Pour voir comment libeio est utilisé avec node.js afin de créer des modules asynchrones, vous devriez voir cet exemple .

En gros, dans node.js, la boucle v8 s'exécute et gère toutes les parties javascript ainsi que les modules C++ [lorsqu'ils s'exécutent dans un thread principal (selon la documentation officielle, node.js est à thread unique)]. En dehors du thread principal, libev et libeio le gèrent dans le pool de threads et libev assure l'interaction avec la boucle principale. Donc, si j'ai bien compris, node.js a une boucle d'événements permanente: c'est la boucle d'événements v8. Pour gérer les tâches asynchrones C++, il utilise un pool de threads [via libeio & libev].

Par exemple:

eio_custom(Task,FLAG,AfterTask,Eio_REQUEST);

Ce qui apparaît dans tous les modules appelle généralement la fonction Task dans le pool de threads. Une fois terminé, il appelle la fonction AfterTask dans le thread principal. Alors que Eio_REQUEST est le gestionnaire de requêtes qui peut être une structure/objet dont le but est de fournir une communication entre le pool de threads et le thread principal.

160
ShrekOverflow

On dirait que certaines des entités discutées (par exemple: libev, etc.) ont perdu de leur pertinence, en raison du fait que cela fait longtemps, mais je pense que la question a encore un grand potentiel.

Permettez-moi d'essayer d'expliquer le fonctionnement du modèle événementiel à l'aide d'un exemple abstrait, dans un environnement UNIX abstrait, dans le contexte de Node, à compter d'aujourd'hui.

Perspective du programme:

  • Le moteur de script commence l'exécution du script.
  • Chaque fois qu'une opération liée à la CPU est rencontrée, elle est exécutée en ligne (machine réelle), dans son intégralité.
  • Chaque fois qu'une opération liée à une entrée/sortie est rencontrée, la demande et son gestionnaire d'achèvement sont enregistrés avec un 'mécanisme d'événement' (machine virtuelle).
  • Répétez les opérations de la même manière ci-dessus jusqu'à la fin du script. Opération liée à la CPU - exécute les demandes en ligne, liées à I/O, demande à la machine comme ci-dessus.
  • Une fois les E/S terminées, les écouteurs sont rappelés.

Le mécanisme d'événement ci-dessus est appelé cadre de boucle d'événement libuv AKA. Node exploite cette bibliothèque pour implémenter son modèle de programmation piloté par les événements.

Point de vue du nœud:

  • Avoir un thread pour héberger le runtime.
  • Prenez le script utilisateur.
  • Compilez-le en natif [effet de levier v8]
  • Chargez le binaire et sautez dans le point d’entrée.
  • Le code compilé exécute les activités liées à la CPU en ligne, en utilisant des primitives de programmation.
  • De nombreux codes liés aux entrées/sorties et aux minuteries ont des wraps natifs. Par exemple, E/S réseau.
  • Les appels d'E/S sont donc acheminés du script aux ponts C++, le descripteur d'E/S et le gestionnaire d'achèvement étant transmis en tant qu'arguments.
  • Le code natif exerce la boucle libuv. Il acquiert la boucle, met en file d'attente un événement de bas niveau représentant l'E/S et un wrapper de rappel natif dans la structure de boucle libuv.
  • Le code natif revient au script - aucune E/S n’a lieu pour le moment!
  • Les éléments ci-dessus sont répétés plusieurs fois, jusqu'à ce que tous les codes non-I/O soient exécutés et que tous les codes I/O soient enregistrés dans libuv.
  • Enfin, quand il ne reste plus rien à exécuter dans le système, le noeud passe le contrôle à libuv
  • libuv entre en action, il récupère tous les événements enregistrés, interroge le système d'exploitation pour obtenir leur fonctionnement.
  • Ceux qui sont prêts pour les E/S en mode non bloquant sont pris en charge, les E/S effectuées et leurs rappels émis. L'un après l'autre.
  • Celles qui ne sont pas encore prêtes (par exemple une lecture de socket pour laquelle l'autre extrémité n'a encore rien écrit) continueront à être interrogées avec le système d'exploitation jusqu'à ce qu'elles soient disponibles.
  • La boucle maintient en interne une minuterie toujours croissante. Lorsque l'application demande un rappel différé (tel que setTimeout), cette valeur de minuterie interne est utilisée pour calculer le bon moment pour déclencher le rappel.

Alors que la plupart des fonctionnalités sont traitées de cette manière, certaines (versions asynchrones) des opérations sur les fichiers sont effectuées à l’aide de threads supplémentaires, bien intégrés dans libuv. Tandis que les opérations d'E/S réseau peuvent attendre en attendant un événement externe tel que l'autre point de terminaison répondant aux données, etc., les opérations sur les fichiers nécessitent du travail du noeud lui-même. Par exemple, si vous ouvrez un fichier et attendez que le fd soit prêt avec les données, cela ne se produira pas, car personne ne lit actuellement! En même temps, si vous lisez le fichier en ligne dans le thread principal, il peut potentiellement bloquer d'autres activités du programme et créer des problèmes évidents, car les opérations sur les fichiers sont très lentes par rapport aux activités liées à l'unité centrale. Ainsi, les threads de travail internes (configurables via la variable d'environnement UV_THREADPOOL_SIZE) sont utilisés pour opérer sur les fichiers, tandis que l'abstraction pilotée par les événements fonctionne de manière intacte, du point de vue du programme.

J'espère que cela t'aides.

17
Gireesh Punathil

Une introduction à libuv

Le projet node.js a débuté en 2009 sous la forme d'un environnement JavaScript découplé du navigateur. En utilisant V8 de Google et libev de Marc Lehmann, node.js a combiné un modèle d’E/S - evented - avec un langage bien adapté au style de programmation; en raison de la façon dont il a été façonné par les navigateurs. Au fur et à mesure que la popularité de node.js augmentait, il était important de le faire fonctionner sous Windows, mais libev ne fonctionnait que sous Unix. L'équivalent Windows des mécanismes de notification des événements du noyau tels que kqueue ou (e) poll est IOCP. libuv était une abstraction autour de libev ou IOCP selon la plate-forme, fournissant aux utilisateurs une API basée sur libev. Dans la version noeud-v0.9.0 de libuv libev a été supprimé .

Également une image décrivant la boucle d’événement dans Node.js par @ BusyRich


Mise à jour du 09/05/2017

Selon cette doc Node.js event loop

Le diagramme suivant montre une vue d'ensemble simplifiée de l'ordre des opérations de la boucle d'événement.

┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘

remarque: chaque case sera appelée "phase" de la boucle d'événement.

Aperçu des phases

  • timers: cette phase exécute les rappels planifiés par setTimeout() et setInterval().
  • Rappels I/O: exécute presque tous les rappels, à l'exception de 
  • close callbacks, ceux programmés par des minuteries et setImmediate(). idle, prepare: utilisé uniquement en interne.
  • poll: récupère les nouveaux événements d'E/S; Le noeud bloquera ici si nécessaire.
  • check: setImmediate() sont rappelés ici.
  • rappels rapprochés: par exemple socket.on('close', ...).

Entre chaque exécution de la boucle d'événements, Node.js vérifie s'il attend des entrées/sorties asynchrones ou des timers et s'arrête proprement s'il n'y en a pas.

15
zangw

Il y a une boucle d'événement dans l'architecture NodeJs.

Modèle de boucle d'événement Node.js

Les applications nodales s'exécutent dans un modèle mono-threadé piloté par les événements. Cependant, Node implémente un pool de threads en arrière-plan afin que le travail puisse être effectué.

Node.js ajoute du travail à une file d'attente d'événements, puis un seul thread exécutant une boucle d'événements le récupère. La boucle d'événements récupère l'élément en haut de la file d'attente, l'exécute, puis saisit l'élément suivant. 

Lors de l'exécution de code ayant une durée de vie plus longue ou un blocage d'E/S, au lieu d'appeler directement la fonction, la fonction est ajoutée à la file d'attente des événements avec un rappel qui sera exécuté une fois la fonction terminée. Lorsque tous les événements de la file d'attente d'événements Node.js ont été exécutés, l'application Node.js se termine.

La boucle d’événements commence à rencontrer des problèmes lorsque nos fonctions d’application se bloquent sur les E/S.

Node.js utilise des rappels d’événements pour éviter d’attendre le blocage des E/S. Par conséquent, toutes les demandes qui exécutent des E/S bloquantes sont effectuées sur un autre thread en arrière-plan. 

Lorsqu'un événement qui bloque les E/S est extrait de la file d'attente, Node.js récupère un thread du pool de threads et exécute la fonction à la place du thread de boucle d'événement principal. Cela empêche les E/S de blocage de bloquer le reste des événements dans la file d'attente des événements.

11
Peter Hauge

Il n'y a qu'une seule boucle d'événement fournie par libuv, V8 n'est qu'un moteur d'exécution JS.

8
Warren Zhou

La fonction pbkdf2 a l'implémentation JavaScript, mais elle délègue en réalité tout le travail à effectuer du côté C++.

env->SetMethod(target, "pbkdf2", PBKDF2);
  env->SetMethod(target, "generateKeyPairRSA", GenerateKeyPairRSA);
  env->SetMethod(target, "generateKeyPairDSA", GenerateKeyPairDSA);
  env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC);
  NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE);
  NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingSEC1);
  NODE_DEFINE_CONSTANT(target, kKeyFormatDER);
  NODE_DEFINE_CONSTANT(target, kKeyFormatPEM);
  NODE_DEFINE_CONSTANT(target, kKeyTypeSecret);
  NODE_DEFINE_CONSTANT(target, kKeyTypePublic);
  NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);
  env->SetMethod(target, "randomBytes", RandomBytes);
  env->SetMethodNoSideEffect(target, "timingSafeEqual", TimingSafeEqual);
  env->SetMethodNoSideEffect(target, "getSSLCiphers", GetSSLCiphers);
  env->SetMethodNoSideEffect(target, "getCiphers", GetCiphers);
  env->SetMethodNoSideEffect(target, "getHashes", GetHashes);
  env->SetMethodNoSideEffect(target, "getCurves", GetCurves);
  env->SetMethod(target, "publicEncrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
                                         EVP_PKEY_encrypt_init,
                                         EVP_PKEY_encrypt>);
  env->SetMethod(target, "privateDecrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
                                         EVP_PKEY_decrypt_init,
                                         EVP_PKEY_decrypt>);
  env->SetMethod(target, "privateEncrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
                                         EVP_PKEY_sign_init,
                                         EVP_PKEY_sign>);
  env->SetMethod(target, "publicDecrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
                                         EVP_PKEY_verify_recover_init,
                                         EVP_PKEY_verify_recover>);

ressource: https://github.com/nodejs/node/blob/master/src/node_crypto.cc

Le module Libuv a une autre responsabilité qui concerne certaines fonctions très particulières de la bibliothèque standard.

Pour certains appels de fonction de bibliothèque standard, le côté Nœud C++ et Libuv décident de faire des calculs coûteux en dehors de la boucle d’événements.

Au lieu de cela, ils utilisent ce qu'on appelle un pool de threads, qui est une série de quatre threads pouvant être utilisés pour exécuter des tâches coûteuses en calcul, telles que la fonction pbkdf2.

Par défaut, Libuv crée 4 threads dans ce pool de threads.

Outre les threads utilisés dans la boucle d'événement, quatre autres threads peuvent être utilisés pour décharger des calculs coûteux qui doivent être effectués dans notre application.

De nombreuses fonctions incluses dans la bibliothèque standard de nœuds utilisent automatiquement ce pool de threads. La fonction pbkdf2 en est une.

La présence de ce pool de threads est très significative.

Donc, Node n'est pas vraiment à un seul thread, car il utilise d'autres threads pour effectuer certaines tâches coûteuses en calcul.

Si le pool d'événements était responsable de l'exécution de la tâche coûteuse en calculs, notre application Nœud ne pourrait rien faire d'autre.

Notre CPU exécute toutes les instructions dans un thread, une par une.

En utilisant le pool de threads, nous pouvons effectuer d'autres opérations dans une boucle d'événements pendant les calculs.

0
Daniel