Question Comment le nœud traite-t-il les requêtes simultanées?


Je lisais récemment nodejs, essayant de comprendre comment il gère plusieurs requêtes simultanées, je sais que nodejs est une architecture basée sur une seule boucle d’événements, à un moment donné, une seule instruction sera exécutée sur le thread principal et le code de blocage / Les appels IO sont gérés par les threads de travail (la valeur par défaut est 4).

Maintenant, ma question est: que se passe-t-il lorsqu'un serveur Web construit à l'aide de nodejs reçoit plusieurs demandes, je sais, qu'il y a beaucoup de threads de dépassement de Stack qui ont des questions similaires, mais n'ont pas trouvé de réponse concrète à cela.

Donc, je mets un exemple ici, disons que nous avons le code suivant dans une route comme /indice.

app.use('/index', function(req, res, next) {

    console.log("hello index routes was invoked");

    readImage("path", function(err, content) {
        status = "Success";
        if(err) {
            console.log("err :", err);
            status = "Error"
        }
        else {
            console.log("Image read");
        }
        return res.send({ status: status });
    });

    var a = 4, b = 5;
    console.log("sum =", a + b);
});

Supposons que readImage () prenne environ 1 min pour lire cette image. Si deux requêtes T1 et T2 sont venues concurremment, Comment nodejs va-t-il traiter ces requêtes?

Va-t-il prendre la première requête T1, la traiter tout en mettant la requête T2 en file d'attente (corrigez-moi si ma compréhension est fausse ici), si un élément asynchrone / bloquant est rencontré comme readImage, il l'envoie ensuite au thread de travail (un peu plus tard, lorsque le processus asynchrone est terminé, il notifie que le thread principal et le thread principal commencent à exécuter le callback). code. Quand est-il fait avec T1 alors prend la demande T2? Est-ce correct? ou il peut traiter le code T2 entre (ce qui signifie que lorsque readImage est appelé, il peut commencer à traiter T2)?

J'apprécierais vraiment que quelqu'un puisse m'aider à trouver une réponse à cette question


16
2017-07-24 10:07


origine


Réponses:


Votre confusion pourrait venir de ne pas vous concentrer suffisamment sur la boucle de l’événement. clairement vous avez une idée de la façon dont cela fonctionne, mais peut-être pas l'image complète.

Partie 1, Bases de la boucle d'événement

Lorsque vous appelez le use méthode, ce qui se passe dans les coulisses est un autre thread est créé pour écouter les connexions.

Cependant, quand une requête arrive, parce que nous sommes dans un thread différent du moteur V8 (et ne peut pas appeler directement la fonction route), un appel sérialisé à la fonction est ajouté au partagé boucle d'événement, pour qu'elle soit appelée plus tard. (la boucle d'événement est un mauvais nom dans ce contexte, car elle fonctionne plus comme une file d'attente ou une pile)

À la fin du fichier js, V8 vérifiera s'il y a des theads ou des messages en cours d'exécution dans la boucle d'événements. S'il n'y en a pas, il quittera 0 (c'est pourquoi le code du serveur maintient le processus en cours). Donc, la première nuance Timing à comprendre est que aucune requête ne sera traitée tant que la fin synchrone du fichier js n'est pas atteinte.

Si la boucle d'événement a été ajoutée au démarrage du processus, chaque appel de fonction sur la boucle d'événement sera traité un par un, dans son intégralité, de manière synchrone.

Pour simplifier, permettez-moi de décomposer votre exemple en quelque chose de plus expressif.

function callback() {
    setTimeout(function inner() {
        console.log('hello inner!');
    }, 0); // †
    console.log('hello callback!');
}

setTimeout(callback, 0);
setTimeout(callback, 0);

setTimeout avec un temps de 0, c'est un moyen rapide et facile de mettre quelque chose sur la boucle de l'événement sans aucune complication du minuteur, car peu importe quoi, il a toujours été au moins de 0 ms.

Dans cet exemple, le résultat sera toujours:

hello callback!
hello callback!
hello inner!
hello inner!

Les deux appels sérialisés à callback sont ajoutés à la boucle d'événement avant que l'un d'eux soit appelé, garanti. Cela se produit car rien ne peut être appelé à partir de la boucle d'événements avant l'exécution synchrone complète du fichier.

Il peut être utile de penser à l’exécution de votre fichier, comme première chose sur la boucle d’événement. Étant donné que chaque invocation à partir de la boucle d'événement ne peut se produire qu'en série, il devient logique que l'invocation d'une autre boucle d'événements puisse se produire pendant son exécution. Seulement quand c'est fini, une autre fonction de boucle d'événement peut être appelée.

Partie 2, le rappel interne

La même logique s'applique également au rappel interne et peut être utilisée pour expliquer pourquoi le programme ne sortira jamais:

hello callback!
hello inner!
hello callback!
hello inner!

Comme vous pouvez vous y attendre.

À la fin de l'exécution du fichier, 2 appels de fonctions sérialisés seront sur la boucle d'événements, à la fois pour callback. Comme la boucle d’événement est un FIFO (premier entré, premier sorti), le setTimeout qui est venu en premier, sera invoqué en premier.

La première chose callback est-ce que faire un autre setTimeout. Comme précédemment, cela ajoutera un appel sérialisé, cette fois à la inner fonction, à la boucle de l'événement. setTimeout retourne immédiatement, et l'exécution passera à la première console.log.

À ce stade, la boucle d'événement ressemble à ceci:

1 [callback] (executing)
2 [callback] (next in line)
3 [inner]    (just added by callback)

Le retour de callback est le signal de la boucle d'événement pour supprimer cette invocation de lui-même. Cela laisse 2 choses sur la boucle de l’événement maintenant: 1 autre appel à callback, et 1 appel à inner.

callback est la prochaine fonction en ligne, elle sera donc appelée ensuite. Le processus se répète. Un appel à inner est ajouté à la boucle d'événement. UNE console.log estampes Hello Callback! et nous finissons en supprimant cette invocation de callback de la boucle d'événement.

Cela laisse la boucle d'événement avec 2 fonctions supplémentaires:

1 [inner]    (next in line)
2 [inner]    (added by most recent callback)

Aucune de ces fonctions ne gêne la boucle des événements, elles s'exécutent les unes après les autres; Le second attend le retour du premier. Ensuite, lorsque le second retourne, la boucle d'événement est laissée vide. Cela combiné avec le fait qu'il n'y a pas d'autres threads en cours d'exécution, déclenche la fin du processus. sortie 0.

Partie 3, relative à l'exemple original

La première chose qui se passe dans votre exemple, c'est qu'un thread est créé dans le processus pour créer un serveur lié à un port particulier. Notez que cela se produit dans C ++ précompilé, pas javascript, et n'est pas un processus séparé, c'est un thread dans le même processus. voir: Didacticiel sur les threads C ++

Alors maintenant, chaque fois qu'une requête arrive, l'exécution de votre code d'origine ne sera pas perturbée. Au lieu de cela, les demandes de connexion entrantes seront ouvertes, conservées et ajoutées à la boucle d'événements.

le use fonction, est la passerelle dans la capture des événements pour les demandes entrantes. C'est une couche d'abstraction, mais par souci de simplicité, c'est utile pour penser à la usefonctionner comme vous le feriez setTimeout. Sauf que, au lieu d'attendre un laps de temps défini, il ajoute le rappel à la boucle d'événements sur les requêtes HTTP entrantes.

Supposons donc que deux requêtes arrivent sur le serveur: T1 et T2. Dans votre question, vous dites qu’ils interviennent en même temps, puisque c’est techniquement impossible, je vais supposer qu’ils se succèdent, avec un temps négligeable entre eux.

Quelle que soit la demande qui arrive en premier, elle sera traitée en premier par le thread secondaire précédent. Une fois la connexion ouverte, elle est ajoutée à la boucle d'événement et nous passons à la requête suivante, puis répétons.

À n'importe quel moment après l'ajout de la première requête à la boucle d'événement, V8 peut commencer l'exécution du use rappeler.


un rapide côté de readImage

Comme il n'est pas clair si readImage est d'une bibliothèque particulière, quelque chose que vous avez écrit ou autrement, il est impossible de dire exactement ce qu'il va faire dans ce cas. Il n'y a que 2 possibilités, alors les voici:

// in this example definition of readImage, its entirely
// synchronous, never using an alternate thread or the
// event loop
function readImage (path, callback) {
    let image = fs.readFileSync(path);
    callback(null, image);
    // a definition like this will force the callback to
    // fully return before readImage returns. This means
    // means readImage will block any subsequent calls.
}

// in this alternate example definition its entirely
// asynchronous, and take advantage of fs' async
// callback.
function readImage (path, callback) {
    fs.readFile(path, (err, data) => {
        callback(err, data);
    });
    // a definition like this will force the readImage
    // to immediately return, and allow exectution
    // to continue.
}

À des fins d'explication, je vais opérer en supposant que readImage retournera immédiatement, comme le devraient les fonctions asynchrones appropriées.


Une fois la use l'exécution du rappel est lancée, les événements suivants se produiront:

  1. Le premier journal de la console s'imprimera.
  2. readImage va lancer un thread de travail et retourner immédiatement.
  3. Le deuxième journal de la console s'imprimera.

Pendant tout ce temps, il est important de noter que ces opérations se produisent de manière synchrone; Aucune autre invocation de boucle d'événement ne peut être lancée tant que ceux-ci ne sont pas terminés. readImage peut être asynchrone, mais l'appel n'est pas, le rappel et l'utilisation d'un thread de travail est ce qui le rend asynchrone.

Après ça use callback return, la requête suivante a probablement déjà terminé l'analyse et a été ajoutée à la boucle d'événement, tandis que V8 était occupé à effectuer les journaux de la console et à l'appel readImage.

Donc la prochaine use callback est invoqué et répète le même processus: se connecter, lancer un thread readImage, se reconnecter, retourner.

Après ce point, les images lues (en fonction de leur durée) ont probablement déjà récupéré ce dont elles avaient besoin et ajouté leur rappel à la boucle d'événement. Donc, ils seront exécutés ensuite, dans l'ordre de celui qui a récupéré ses données en premier. rappelez-vous, ces opérations se produisaient dans des threads distincts, de sorte que non seulement le javascript principal était parallèle au thread principal, mais aussi parallèle les unes aux autres, donc peu importe lequel a été appelé en premier. dibs sur la boucle d'événement.

Quel que soit le premier readImage terminé sera le premier à être exécuté. alors, sans erreurs, nous allons imprimer à la console, puis écrire à la réponse pour la demande correspondante, tenue dans le champ lexical.

Lorsque cet envoi est renvoyé, le prochain rappel readImage commencera à exécuter le journal de la console et à écrire dans la réponse.

à ce stade, les deux threads readImage sont morts et la boucle d'événement est vide, mais le thread qui contient la liaison du port du serveur maintient le processus actif, en attendant que quelque chose d'autre soit ajouté à la boucle d'événement et que le cycle continue.

J'espère que cela vous aidera à comprendre la mécanique derrière la nature asynchrone de l'exemple que vous avez fourni


6
2017-07-30 17:26



Pour chaque demande entrante, le noeud le traitera un par un. Cela signifie qu'il doit y avoir de l'ordre, tout comme la file d'attente, en premier servi. Lorsque le nœud commence à traiter la demande, tout le code synchrone s'exécute et asynchrone passe au thread de travail, donc le nœud peut commencer à traiter la requête suivante. Lorsque la partie asynchrone est terminée, elle retourne au thread principal et continue.

Ainsi, lorsque votre code synchrone prend trop de temps, vous bloquez le thread principal, le nœud ne sera pas en mesure de gérer d'autres requêtes, il est facile à tester.

app.use('/index', function(req, res, next) {
    // synchronous part
    console.log("hello index routes was invoked");
    var sum = 0;
    // useless heavy task to keep running and block the main thread
    for (var i = 0; i < 100000000000000000; i++) {
        sum += i;
    }
    // asynchronous part, pass to work thread
    readImage("path", function(err, content) {
        // when work thread finishes, add this to the end of the event loop and wait to be processed by main thread
        status = "Success";
        if(err) {
            console.log("err :", err);
            status = "Error"
        }
        else {
            console.log("Image read");
        }
        return res.send({ status: status });
    });
    // continue synchronous part at the same time.
    var a = 4, b = 5;
    console.log("sum =", a + b);
});

Node ne commencera pas à traiter la requête suivante avant d'avoir terminé toute la partie synchrone. Donc, les gens ont dit ne pas bloquer le thread principal.


3
2017-07-28 08:11



Vous pouvez simplement créer un processus enfant en déplaçant la fonction readImage () dans un fichier différent en utilisant fork ().

Le fichier parent, parent.js:

const { fork } = require('child_process');
const forked = fork('child.js');
forked.on('message', (msg) => {
   console.log('Message from child', msg);
});

forked.send({ hello: 'world' });

Le fichier enfant, child.js:

process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);

L'article ci-dessus pourrait vous être utile.

Dans le fichier parent ci-dessus, nous allons child.js (qui exécutera le fichier avec la commande node) et ensuite nous écoutons le message un événement. le message l'événement sera émis chaque fois que l'enfant utilisera process.send, ce que nous faisons chaque seconde.

Pour transmettre des messages du parent à l’enfant, nous pouvons exécuter le send fonction sur l'objet forked lui-même, puis, dans le script enfant, nous pouvons écouter le message événement sur le global process objet.

Lors de l'exécution du parent.js fichier ci-dessus, il va d'abord envoyer le { hello: 'world' } objet à imprimer par le processus fils forked, puis le processus fils forked enverra une valeur de compteur incrémentée chaque seconde à imprimer par le processus parent.


2
2017-07-24 10:38



Il y a un certain nombre d'articles qui expliquent cela tels que celui-là

Le long et le court de c'est que nodejs n'est pas vraiment une application unique, c'est une illusion. Le diagramme en haut du lien ci-dessus l'explique raisonnablement bien, mais en résumé

  • La boucle d'événements NodeJS s'exécute dans un seul thread
  • Quand il reçoit une demande, il transmet cette demande à un nouveau sujet.

Ainsi, dans votre code, votre application en cours d'exécution aura un PID de 1 par exemple. Lorsque vous recevez la requête T1, elle crée le PID 2 qui traite cette demande (en 1 minute). Pendant que vous courez, vous obtenez la requête T2 qui génère le PID 3 en 1 minute. Les deux PID 2 et 3 se termineront une fois leur tâche terminée, mais le PID 1 continuera à écouter et à distribuer les événements au fur et à mesure de leur arrivée.

En résumé, NodeJS être 'single threaded' est vrai, mais c'est juste un écouteur de boucle d'événements. Lorsque des événements sont entendus (requêtes), il les transmet à un pool de threads qui s'exécutent de manière asynchrone, ce qui signifie qu'il ne bloque pas les autres requêtes.


0
2017-07-27 05:49



L'interface V8 JS (c.-à-d.: Node) est fondamentalement mono-threadée. Mais les processus qu'il déclenche peuvent être asynchrones, par exemple: 'fs.readFile'.

Au fur et à mesure que le serveur express s'exécute, il ouvre de nouveaux processus pour répondre aux requêtes. La fonction 'readImage' sera donc lancée (généralement de manière asynchrone), ce qui signifie qu'elle sera renvoyée dans n'importe quel ordre. Cependant, le serveur gérera automatiquement la réponse à chaque demande.

Donc, vous ne devrez pas gérer qui readImage la réponse va à quelle demande.

Donc, fondamentalement, T1 et T2, ne reviendront pas simultanément, cela est pratiquement impossible. Ils sont tous deux fortement dépendants du système de fichiers pour compléter la lecture et ils peuvent finir dans N'IMPORTE QUEL ORDRE (cela ne peut pas être prédit). Notez que les processus sont gérés par la couche du système d'exploitation et sont par nature multithread (sur un ordinateur moderne).

Si vous recherchez un système de files d'attente, il ne devrait pas être trop difficile d'implémenter / de vous assurer que les images sont lues / retournées dans l'ordre exact dans lequel elles sont demandées.


0
2017-08-02 16:15



Comme il n'y a pas vraiment plus à ajouter à la réponse précédente de Marcus - voici un graphique qui explique le mécanisme de boucle d'événements à thread unique:

enter image description here


0
2017-08-02 16:20