296 votes

Comment Node.js est-il intrinsèquement plus rapide alors qu'il repose toujours sur Threads en interne ?

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

Principalement, à un moment donné, Ryan Dahl (le créateur de Node.js) dit que Node.js est basé sur des boucles d'événements plutôt que sur des threads. Les threads sont coûteux et ne devraient être utilisés que par les experts de la programmation concurrente.

Plus tard, il montre la pile d'architecture de Node.js qui a une implémentation C sous-jacente qui a son propre pool de threads en interne. Il est donc évident que les développeurs de Node.js ne lanceraient jamais leurs propres threads ou n'utiliseraient 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 les threads... il ne fait que cacher l'implémentation. Comment cela peut-il être plus rapide si 50 personnes demandent 50 fichiers (pas actuellement en mémoire), alors 50 threads ne sont-ils pas nécessaires ?

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

Ainsi, ne vous contentez-vous pas de prendre un problème (le threading) et de le dissimuler alors que ce problème existe toujours : principalement les threads multiples, le changement de contexte, les blocages... etc.

Il doit y avoir un détail que je ne comprends toujours pas ici.

148voto

jrtipton Points 1694

Il y a en fait plusieurs choses différentes qui sont confondues ici. Mais ça commence avec le mème que les fils sont juste très durs. Donc s'ils sont difficiles, vous êtes plus susceptible, lorsque vous utilisez des threads de 1) casser à cause de bogues et 2) de ne pas les utiliser aussi efficacement que possible. Le point (2) est celui qui fait l'objet de votre question.

Pensez à l'un des exemples qu'il donne, où une demande arrive et vous exécutez une requête, puis vous faites quelque chose avec les résultats de cette requête. Si vous l'écrivez d'une 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 l'arrivée de la demande a entraîné la création d'un nouveau fil d'exécution qui a exécuté le code ci-dessus, vous aurez un fil d'exécution qui ne fait rien du tout pendant que query() est en cours d'exécution. (Apache, selon Ryan, utilise un seul thread pour satisfaire la requête originale alors que nginx le surpasse dans les cas dont il parle, car ce n'est pas le cas).

Si vous étiez vraiment intelligent, vous pourriez exprimer le code ci-dessus de manière à ce que l'environnement puisse faire autre chose pendant l'exécution de la requête :

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

C'est en gros ce que fait node.js. Vous décorez essentiellement - d'une manière qui est pratique en raison du langage et de l'environnement, d'où les points sur les fermetures - votre code de telle sorte que l'environnement peut être intelligent sur ce qui s'exécute, et quand. De cette façon, node.js n'est pas nouveau dans le sens où il a inventé l'E/S asynchrone (non pas que quelqu'un ait revendiqué quelque chose comme ça), mais il est nouveau dans le sens où la façon dont il est exprimé est un peu différente.

Note : quand je dis que l'environnement peut être intelligent à propos de ce qui s'exécute et quand, ce que je veux dire spécifiquement c'est que le thread utilisé pour commencer une E/S peut maintenant être utilisé pour gérer une autre requête, ou un calcul qui peut être fait en parallèle, ou commencer une autre E/S parallèle. (Je ne suis pas certain que le nœud soit assez sophistiqué pour commencer plus de travail pour la même requête, mais vous comprenez l'idée).

32voto

nalply Points 4675

Note ! C'est une vieille réponse. Bien qu'elle soit toujours vraie dans ses grandes lignes, certains détails ont pu changer en raison du développement rapide de Node au cours des dernières années.

Il utilise des fils parce que :

  1. El L'option O_NONBLOCK de open() ne fonctionne pas sur les fichiers .
  2. Il existe des bibliothèques tierces qui ne proposent pas d'entrées-sorties non bloquantes.

Pour simuler des entrées/sorties non bloquantes, des threads sont nécessaires : les entrées/sorties bloquantes sont effectuées dans un thread séparé. C'est une solution peu élégante qui entraîne une surcharge importante.

C'est encore pire au niveau du matériel :

  • Avec DMA le CPU décharge les entrées-sorties de manière asynchrone.
  • Les données sont transférées directement entre le dispositif d'E/S et la mémoire.
  • Le noyau enveloppe cela dans un appel système synchrone et bloquant.
  • Node.js enveloppe l'appel système bloquant dans un thread.

C'est tout simplement stupide et inefficace. Mais au moins, ça marche ! Nous pouvons apprécier Node.js parce qu'il cache les détails laids et encombrants derrière une architecture asynchrone pilotée par les événements.

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

Editar: J'en ai discuté avec un ami et il m'a dit qu'une alternative aux fils est le sondage avec sélectionnez : spécifier un timeout de 0 et faire IO sur les descripteurs de fichiers retournés (maintenant qu'ils sont garantis de ne pas bloquer).

29voto

Toby Eggitt Points 215

Je crains de faire une erreur, si c'est le cas, effacez-moi et je m'excuse. En particulier, je ne vois pas comment je crée les petites annotations soignées que certains ont créées. Cependant, j'ai de nombreuses préoccupations/observations à faire sur ce fil.

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

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

est essentiellement bidon. Si le fil est en train de calculer, alors il ne se tourne pas les pouces, il fait le travail nécessaire. Si, d'autre part, il attend simplement la fin d'une entrée/sortie, alors il est pas en utilisant le temps CPU, l'objectif de l'infrastructure de contrôle des threads dans le noyau est que le CPU trouve quelque chose d'utile à faire. La seule façon de "se tourner les pouces" comme suggéré ici serait de créer une boucle d'interrogation, et personne qui a codé un vrai serveur web n'est assez inepte pour le faire.

2) "Les threads sont difficiles", n'a de sens que dans le contexte du partage des données. Si vous avez des threads essentiellement indépendants, comme c'est le cas lorsque vous traitez des requêtes web indépendantes, alors le threading est trivialement simple, il suffit de coder le flux linéaire de la façon de traiter une tâche, et de rester assis en sachant qu'il traitera plusieurs requêtes, et que chacune sera effectivement indépendante. Personnellement, je dirais que pour la plupart des programmeurs, l'apprentissage du mécanisme de fermeture/callback est plus complexe que le simple codage de la version top-to-bottom du thread. (Mais oui, si vous devez communiquer entre les threads, la vie devient très vite difficile, mais je ne suis pas convaincu que le mécanisme de closure/callback change vraiment cela, il limite juste vos options, parce que cette approche est toujours réalisable avec les threads. De toute façon, c'est une toute autre discussion qui n'est pas vraiment pertinente ici).

3) Jusqu'à présent, personne n'a présenté de véritable preuve de la raison pour laquelle un type particulier de changement de contexte prendrait plus ou moins de temps qu'un autre type. Mon expérience dans la création de noyaux multi-tâches (à petite échelle pour des contrôleurs embarqués, rien d'aussi sophistiqué qu'un "vrai" système d'exploitation) suggère que ce ne serait pas le cas.

4) Toutes les illustrations que j'ai vues jusqu'à présent et qui prétendent montrer à quel point Node est plus rapide que les autres serveurs web sont horriblement défectueuses, cependant, elles sont défectueuses d'une manière qui illustre indirectement un avantage que j'accepterais définitivement pour Node (et ce n'est en aucun cas insignifiant). Node ne semble pas avoir besoin (ni même permettre, en fait) de s'accorder. Si vous avez un modèle threadé, vous devez créer suffisamment de threads pour gérer la charge attendue. Si vous le faites mal, vous obtiendrez des performances médiocres. S'il y a trop peu de threads, le processeur est inactif, mais incapable d'accepter d'autres demandes. Si vous créez trop de threads, vous gaspillerez la mémoire du noyau et, dans le cas d'un environnement Java, vous gaspillerez également la mémoire du tas principal. Or, pour Java, gaspiller du heap est le premier, le meilleur moyen de foutre en l'air les performances du système, parce qu'un garbage collector efficace (actuellement, cela pourrait changer avec G1, mais il semble que le jury n'ait pas encore tranché sur ce point au début de 2013 au moins) dépend de la disponibilité de beaucoup de heap. Donc, voilà le problème, réglez-le avec trop peu de threads, vous avez des processeurs inactifs et un faible débit, réglez-le avec trop de threads, et il s'enlise d'autres façons.

5) Il y a une autre façon dont j'accepte la logique de l'affirmation que l'approche de Node "est plus rapide par conception", et c'est la suivante. La plupart des modèles de threads utilisent un modèle de changement de contexte en tranches de temps, superposé au modèle préemptif plus approprié (alerte au jugement de valeur :) et plus efficace (pas un jugement de valeur). Cela se produit pour deux raisons, premièrement, la plupart des programmeurs ne semblent pas comprendre la préemption prioritaire, et deuxièmement, si vous apprenez le threading dans un environnement Windows, le timeslicing est là que vous le vouliez ou non (bien sûr, cela renforce le premier point ; notamment, les premières versions de Java utilisaient la préemption prioritaire sur les implémentations Solaris, et le timeslicing sous Windows. Comme la plupart des programmeurs ne comprenaient pas et se plaignaient que "le threading ne fonctionne pas sous Solaris", ils ont changé le modèle pour utiliser le timeslice partout). Quoi qu'il en soit, l'essentiel est que le timeslicing crée des commutations de contexte supplémentaires (et potentiellement inutiles). Chaque changement de contexte prend du temps au CPU, et ce temps est effectivement retiré du travail qui peut être fait sur le vrai travail à faire. Cependant, le temps investi dans le changement de contexte à cause du timeslicing ne devrait pas dépasser un très faible pourcentage du temps global, à moins que quelque chose d'extraordinaire ne se produise, et il n'y a aucune raison de s'attendre à ce que ce soit le cas dans un simple serveur web). Donc, oui, les commutations de contexte excessives impliquées dans le timeslicing sont inefficaces (et elles ne se produisent pas dans le cas d'un serveur web simple). Noyau ) mais la différence sera de quelques pourcentages de débit, pas le genre de facteurs entiers qui sont impliqués dans les revendications de performance qui sont souvent impliquées pour Node.

Quoi qu'il en soit, je vous prie de m'excuser si tout cela est long et décousu, mais j'ai vraiment l'impression que jusqu'à présent, la discussion n'a rien prouvé, et je serais heureux d'entendre quelqu'un qui se trouve dans l'une ou l'autre de ces situations :

a) une véritable explication 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) est, je crois, la véritable explication de tous les tests que j'ai vus jusqu'à présent. ([edit], en fait, plus j'y pense, plus je me demande si la mémoire utilisée par un grand nombre de piles pourrait être significative ici. La taille des piles par défaut pour les threads modernes a tendance à être assez énorme, mais la mémoire allouée par un système d'événements basé sur les fermetures serait seulement ce qui est nécessaire).

b) un vrai benchmark qui donne une chance équitable au serveur threadé de son choix. Au moins de cette façon, je devrais arrêter de croire que les revendications sont essentiellement fausses ;> ([edit] c'est probablement un peu plus fort que ce que j'avais l'intention de dire, mais j'ai le sentiment que les explications données pour les avantages en termes de performances sont au mieux incomplètes, et que les benchmarks présentés ne sont pas raisonnables).

A la vôtre, Toby

14voto

Alfred Points 32190

Ce que je ne comprends pas, c'est l'intérêt que Node.js utilise toujours des threads.

Ryan utilise des threads pour les parties qui sont bloquantes (la plupart de node.js utilise des IO non bloquantes) parce que certaines parties sont follement difficiles à écrire de manière non bloquante. Mais je crois que le souhait de Ryan est d'avoir tout non-bloquant. Sur diapositive 63 (conception interne) tu vois que Ryan utilise libev (bibliothèque qui abstrait la notification d'événements asynchrones) pour le système non-bloquant boucle d'événement . Grâce à la boucle d'événements, node.js a besoin de moins de threads, ce qui réduit le changement de contexte, la consommation de mémoire, etc.

12voto

gawi Points 5073

Les threads ne sont utilisés que pour traiter les fonctions qui n'ont pas de possibilité d'asynchronisme, comme par exemple stat() .

El stat() est toujours bloquante, et node.js doit donc utiliser un thread pour effectuer l'appel réel 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 fonctions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X