web-dev-qa-db-fra.com

Comment Node.js est-il intrinsèquement plus rapide lorsqu'il s'appuie toujours sur les threads en interne?

Je viens de regarder la vidéo suivante: Introduction à Node.js et je ne comprends toujours pas comment vous bénéficiez des avantages de la vitesse.

À un moment donné, Ryan Dahl (le créateur de Node.js) dit que Node.js est basé sur une boucle d’événements et non sur des threads. Les threads sont chers et ne devraient être utilisés que par les experts en programmation simultanée.

Plus tard, il montre ensuite la pile d'architecture de Node.js qui a une implémentation C sous-jacente qui possède son propre pool de threads en interne. Il est donc évident que les développeurs de Node.js ne lancent jamais leurs propres threads et n'utilisent pas directement le pool de threads ... ils utilisent des rappels asynchrones. C'est ce que je comprends.

Ce que je ne comprends pas, c’est que Node.js utilise toujours des threads ... c’est juste pour cacher l’implémentation, alors comment est-ce plus rapide si 50 personnes demandent 50 fichiers (qui ne sont pas actuellement en mémoire) et qu’il n’y a donc pas besoin de 50 threads ?

La seule différence est que, puisqu'il est géré en interne, le développeur de Node.js n'a pas à coder les détails des threads, mais qu'il utilise toujours les threads pour traiter les demandes de fichier IO (bloquantes)).

Donc, ne prenez-vous pas vraiment un problème (threading) et le cachez-vous tant que ce problème existe toujours: principalement plusieurs threads, changement de contexte, impasses mortes, etc.?

Il doit y avoir quelques détails que je ne comprends toujours pas ici.

278
Ralph Caraveo

Il y a en fait quelques choses différentes qui sont confondues ici. Mais cela commence avec le meme que les discussions sont vraiment difficiles. Ainsi, s’ils sont difficiles, il est plus probable que vous utilisiez des threads pour 1) casser des bugs et 2) ne pas les utiliser aussi efficacement que possible. (2) est celui que vous demandez.

Pensez à l'un des exemples qu'il cite, où une requête arrive et que vous exécutez une requête, puis faites quelque chose avec les résultats obtenus. Si vous l'écrivez de manière procédurale standard, le code pourrait ressembler à ceci:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Si la requête a entraîné la création d'un nouveau thread qui a exécuté le code ci-dessus, vous en aurez un qui ne fera rien du tout pendant que query() est en cours d'exécution. (Apache, selon Ryan, utilise un seul thread pour satisfaire la requête initiale, tandis que nginx le surpasse dans les cas dont il parle car ce n'est pas le cas.)

Maintenant, si vous étiez vraiment malin, vous devriez exprimer le code ci-dessus de manière à ce que l'environnement puisse fonctionner et faire autre chose pendant que vous exécutez la requête:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

C'est essentiellement ce que fait node.js. En gros, vous décorez - de manière pratique en raison de la langue et de l’environnement, d’où les points relatifs aux fermetures - de votre code de telle sorte que l’environnement puisse être intelligent en ce qui concerne le fonctionnement et le moment. En ce sens, node.js n’est pas nouvea dans le sens où il a inventé les E/S asynchrones (personne n’a jamais prétendu avoir une chose pareille), mais il est nouveau dans le sens où elle est exprimée un peu différent.

Remarque: lorsque je dis que l'environnement peut être intelligent en ce qui concerne le fonctionnement et le moment, ce que je veux dire en particulier, c'est que le thread utilisé pour démarrer certaines E/S peut maintenant être utilisé pour traiter une autre demande ou un calcul pouvant être effectué. en parallèle, ou démarrez une autre E/S parallèle. (Je ne suis pas certain que le noeud est assez sophistiqué pour commencer plus de travail pour la même demande, mais vous avez l'idée.)

137
jrtipton

Remarque! Ceci est une vieille réponse. Bien que cela reste vrai dans les grandes lignes, certains détails ont peut-être changé en raison du développement rapide de Node au cours des dernières années.

Il utilise des threads parce que:

  1. L'option l'option O_NONBLOCK de open () ne fonctionne pas sur les fichiers .
  2. Certaines bibliothèques tierces n'offrent pas d'E/S non bloquantes.

Pour imiter des E/S non bloquantes, les threads sont nécessaires: bloquez IO dans un thread séparé. C'est une solution laide qui génère beaucoup de temps système.

C'est encore pire au niveau matériel:

  • Avec DMA , la CPU décharge de manière asynchrone IO.
  • Les données sont transférées directement entre le IO) et la mémoire.
  • Le noyau encapsule cela dans un appel système synchrone bloquant.
  • Node.js encapsule l'appel système bloquant dans un thread.

C'est tout simplement stupide et inefficace. Mais ça marche au moins! Nous pouvons profiter de Node.js car il cache les détails laids et encombrants d'une architecture asynchrone pilotée par les événements.

Peut-être que quelqu'un implémentera O_NONBLOCK pour les fichiers à l'avenir? ...

Edit: J'en ai parlé à un ami et il m'a dit qu'une alternative aux threads consiste à interroger avec select : spécifier un délai d'attente de 0 et faire IO sur les descripteurs de fichier retournés (maintenant qu'ils sont assurés de ne pas bloquer).

32
nalply

Je crains de "faire la mauvaise chose" ici, si ainsi me supprimer et je m'excuse. En particulier, je ne vois pas comment je crée les petites annotations ordonnées que certains ont créées. Cependant, j'ai de nombreuses préoccupations/observations à formuler sur ce fil.

1) L'élément commenté dans le pseudo-code dans l'une des réponses courantes

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

est essentiellement faux. Si le fil est en train de calculer, il ne s'agit pas de faire tourner les pouces, c'est de faire le travail nécessaire. Si, par contre, il attend simplement la fin de l'IO, c'est pas utilise le temps CPU, le point essentiel de l'infrastructure de contrôle des threads dans le noyau est que le CPU trouvera quelque chose d'utile pour faire. La seule façon de "tourner les pouces", comme suggéré ici, serait de créer une boucle de sondage, et quiconque a codé un vrai serveur Web n'est pas assez capable de le faire.

2) "Les threads sont difficiles" n'a de sens que dans le contexte du partage de données. Si vous avez essentiellement des threads indépendants, comme c'est le cas lors du traitement de requêtes Web indépendantes, le threading est d'une simplicité triviale: il vous suffit de coder le flux linéaire de la façon de traiter un travail et de rester assis, sachant qu'il gérera plusieurs requêtes, sera effectivement indépendant. Personnellement, j’irais à dire que pour la plupart des programmeurs, l’apprentissage du mécanisme de fermeture/rappel est plus complexe que le simple codage de la version de threads de haut en bas. (Mais oui, si vous devez communiquer entre les threads, la vie devient très difficile, mais je ne suis pas convaincu que le mécanisme de fermeture/callback change vraiment cela, il limite simplement vos options, car cette approche est toujours réalisable avec des threads. Quoi qu’il en soit, c’est une toute autre discussion qui n’est vraiment pas pertinente ici).

3) À ce jour, personne n’a présenté de preuve tangible de la raison pour laquelle un type particulier de changement de contexte prendrait plus ou moins de temps que tout autre type. Mon expérience dans la création de noyaux multitâches (à petite échelle pour les contrôleurs intégrés, rien de plus sophistiqué qu'un "vrai" système d'exploitation) suggère que cela ne serait pas le cas.

4) Toutes les illustrations que j’ai vues à ce jour et qui prétendent montrer à quel point Node est plus rapide que d’autres serveurs Web) sont terriblement défectueuses. Cependant, elles ont un défaut qui en est une illustration indirecte avantage j'accepterais certainement pour Node (et ce n'est nullement insignifiant). Node ne semble pas avoir besoin (ni même ne permet, en fait) de réglage Si vous avez un modèle threadé, vous devez créer suffisamment de threads pour gérer la charge attendue. Faites-le mal, et les performances seront médiocres. S'il y a trop de threads, le processeur est inactif, mais ne peut pas accepter plus de demandes, créer trop de threads et vous allez gaspiller de la mémoire du noyau, et dans le cas d'un environnement Java, vous gaspillerez également de la mémoire principale. Maintenant, pour Java, gaspillage heap est le premier, le meilleur moyen de gâcher les performances du système, car une récupération efficace des ordures (pour le moment, cela pourrait changer avec G1, mais il semble que le jury soit toujours sur la question à ce jour. début 2013 au moins) dépend d’avoir beaucoup de réserve. Il y a donc un problème. Réglez-le avec trop peu de threads, vous avez des processeurs inactifs et un débit insuffisant. Réglez-le avec trop de threads et le processus s'enlise d'autres façons.

5) Il y a une autre manière d'accepter la logique de l'affirmation selon laquelle l'approche de Node "est plus rapide par conception", et c'est la suivante. La plupart des modèles de thread utilisent un modèle de commutation de contexte découpé dans le temps, superposé au modèle préemptif plus approprié (alerte de jugement de valeur :) et plus efficace (pas de jugement de valeur). Cela se produit pour deux raisons: premièrement, la plupart des programmeurs ne semblent pas comprendre la préemption des priorités, et deuxièmement, si vous apprenez à utiliser les threads dans un environnement Windows, le décalage temporel existe, que cela vous plaise ou non (bien sûr, cela renforce le premier point ; notamment, les premières versions de Java utilisaient la préemption de priorité sur les implémentations Solaris et le timelicing dans Windows. Parce que la plupart des programmeurs ne comprenaient pas et se plaignaient du fait que "le threading ne fonctionnait pas dans Solaris" Quoi qu'il en soit, le découpage temporel crée des changements de contexte supplémentaires (et potentiellement inutiles). Chaque changement de contexte prend du temps CPU, et ce temps est effectivement supprimé du travail pouvant être effectué sur le vrai travail à Cependant, le temps investi dans le changement de contexte en raison du laps de temps ne devrait pas dépasser un très petit pourcentage du temps total, à moins que quelque chose d'assez bizarre ne se produise, et il n'y a aucune raison que je puisse le faire. voir s’attendre à ce que ce soit le cas dans un simple serveur Web). Donc, oui, les changements de contexte excessifs impliqués dans le découpage temporel sont inefficaces (et cela ne se produit pas dans les threads du noya en règle générale, au fait), mais la différence sera de quelques pour cent du débit, et non du genre. nombre de facteurs entiers impliqués dans les revendications de performances souvent impliquées pour Node.

Quoi qu’il en soit, toutes mes excuses pour la longueur et la rigueur, mais j’ai vraiment le sentiment que jusqu’à présent, la discussion n’a rien prouvé et je serais ravi d’entendre quelqu'un de l'une ou l'autre de ces situations:

a) une explication réelle de la raison pour laquelle Node devrait être meilleur (au-delà des deux scénarios que j'ai décrits ci-dessus, dont le premier (mauvais réglage), je crois, constitue la véritable explication de tous les tests que j'ai J'ai vu jusqu'à présent. En fait, plus j'y pense, plus je me demande si la mémoire utilisée par un grand nombre de piles pourrait être importante ici. Les tailles de pile par défaut pour les threads modernes ont tendance à être assez énorme, mais la mémoire allouée par un système d’événements basé sur la fermeture ne serait que ce qui est nécessaire)

b) une véritable référence qui donne une chance au serveur fileté de choix. Au moins, je devrais cesser de croire que les revendications sont essentiellement fausses;> (c'est-à-dire plutôt fort que je le pensais, mais j'estime que les explications fournies pour les avantages liés aux performances sont, au mieux, incomplètes, les repères indiqués sont déraisonnables).

Cordialement, Toby

28
Toby Eggitt

Ce que je ne comprends pas, c’est que Node.js utilise encore des threads.

Ryan utilise des threads pour les pièces bloquantes (la plupart des noeuds.js utilisent des E/S non bloquantes) car certaines parties sont insensées et difficiles à écrire sans blocage. Mais je crois que Ryan souhaite que tout soit non bloquant. Sur diapositive 63 (conception interne) vous voyez que Ryan utilise libev (bibliothèque qui résume la notification d'événement asynchrone) pour le non-bloquant eventloop . En raison de la boucle d'événement, node.js a besoin de threads inférieurs, ce qui réduit le changement de contexte, la consommation de mémoire, etc.

14
Alfred

Les threads ne sont utilisés que pour traiter des fonctions n'ayant pas de fonction asynchrone, comme stat().

La fonction stat() est toujours bloquante. Par conséquent, node.js doit utiliser un thread pour effectuer l'appel en cours sans bloquer le thread principal (boucle d'événement). Potentiellement, aucun thread du pool de threads ne sera jamais utilisé si vous n'avez pas besoin d'appeler ce type de fonction.

11
gawi

Je ne connais rien au fonctionnement interne de node.js, mais je peux voir comment utiliser une boucle d'événement peut être plus performant que la gestion des E/S threadées. Imaginez une demande de disque, donnez-moi staticFile.x, faites-lui 100 demandes pour ce fichier. Chaque demande prend normalement un thread récupérant ce fichier, soit 100 threads.

Maintenant, imaginez la première demande créant un fil qui devienne un objet éditeur, les 99 autres demandes s’adressant d’abord à un objet éditeur pour staticFile.x. Si c'est le cas, écoutez-le pendant son travail, sinon démarrez un nouveau fil et nouvel objet éditeur.

Une fois que le thread unique est terminé, il passe staticFile.x à tous les 100 auditeurs et se détruit lui-même. La demande suivante crée un nouveau thread et un nouvel objet éditeur.

Donc, dans l'exemple ci-dessus, il s'agit de 100 threads contre 1 thread, mais également 1 recherche de disque au lieu de 100 recherches de disque, le gain peut être assez phénoménal. Ryan est un gars intelligent!

Une autre façon de voir est l'un de ses exemples au début du film. Au lieu de:

pseudo code:
result = query('select * from ...');

Encore une fois, 100 requêtes distinctes à une base de données par rapport à ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Si une requête était déjà lancée, d’autres requêtes équivalentes sautaient simplement dans le train, vous pouvez donc avoir 100 requêtes dans un seul aller-retour à la base de données.

7
BGerrissen