Quand le pool de threads est-il utilisé?

104

J'ai donc une compréhension du fonctionnement de Node.js: il a un seul thread d'écoute qui reçoit un événement et le délègue ensuite à un pool de travailleurs. Le thread de travail notifie l'écouteur une fois qu'il a terminé le travail, et l'écouteur renvoie ensuite la réponse à l'appelant.

Ma question est la suivante: si je lève un serveur HTTP dans Node.js et que j'appelle sleep sur l'un de mes événements de chemin routés (comme "/ test / sleep"), tout le système s'arrête. Même le fil d'écoute unique. Mais je crois comprendre que ce code se produit sur le pool de travailleurs.

Maintenant, en revanche, lorsque j'utilise Mongoose pour parler à MongoDB, les lectures de bases de données sont une opération d'E / S coûteuse. Node semble être capable de déléguer le travail à un thread et de recevoir le rappel lorsqu'il se termine; le temps de chargement à partir de la base de données ne semble pas bloquer le système.

Comment Node.js décide-t-il d'utiliser un thread de pool de threads par rapport au thread d'écoute? Pourquoi ne puis-je pas écrire de code d'événement qui dort et ne bloque qu'un thread de pool de threads?

Haney
la source
@Tobi - J'ai vu ça. Cela ne répond toujours pas à ma question. Si le travail était sur un autre thread, la mise en veille n'affecterait que ce thread et pas l'auditeur également.
Haney
8
Une vraie question, où vous essayez de comprendre quelque chose par vous-même, et quand vous ne trouvez pas de sortie au labyrinthe, vous demandez de l'aide.
Rafael Eyng

Réponses:

241

Votre compréhension du fonctionnement du nœud n'est pas correcte ... mais c'est une idée fausse courante, car la réalité de la situation est en fait assez complexe et se résume généralement à de petites phrases lapidaires comme "le nœud est à un seul thread" qui simplifient à l'extrême les choses .

Pour le moment, nous ignorerons le multi-traitement / multi-thread explicite via le cluster et les threads de webworker , et parlerons simplement du nœud non threadé typique.

Le nœud s'exécute dans une seule boucle d'événements. C'est un thread unique, et vous n'obtenez jamais qu'un seul thread. Tout le javascript que vous écrivez s'exécute dans cette boucle, et si une opération de blocage se produit dans ce code, alors elle bloquera la boucle entière et rien d'autre ne se produira jusqu'à ce qu'elle se termine. Il s'agit de la nature typiquement à thread unique du nœud dont vous entendez tant parler. Mais ce n'est pas une vue d'ensemble.

Certaines fonctions et modules, généralement écrits en C / C ++, prennent en charge les E / S asynchrones. Lorsque vous appelez ces fonctions et méthodes, elles gèrent en interne le passage de l'appel à un thread de travail. Par exemple, lorsque vous utilisez le fsmodule pour demander un fichier, le fsmodule transmet cet appel à un thread de travail, et ce worker attend sa réponse, qu'il présente ensuite à la boucle d'événements qui s'est déclenchée sans elle dans le entre temps. Tout cela est soustrait à vous, le développeur du nœud, et une partie est soustraite aux développeurs de modules grâce à l'utilisation de libuv .

Comme le souligne Denis Dollfus dans les commentaires (de cette réponse à une question similaire), la stratégie utilisée par libuv pour réaliser des E / S asynchrones n'est pas toujours un pool de threads, en particulier dans le cas du httpmodule une stratégie différente semble être utilisé à ce moment. Pour nos besoins ici, il est principalement important de noter comment le contexte asynchrone est atteint (en utilisant libuv) et que le pool de threads maintenu par libuv est l'une des multiples stratégies proposées par cette bibliothèque pour atteindre l'asynchronicité.


Sur une tangente principalement liée, il y a une analyse beaucoup plus profonde de la façon dont le nœud atteint l'asynchronicité, et de certains problèmes potentiels connexes et comment les traiter, dans cet excellent article . La plus grande partie se développe sur ce que j'ai écrit ci-dessus, mais en plus, cela indique:

  • Tout module externe que vous incluez dans votre projet qui utilise le C ++ natif et libuv est susceptible d'utiliser le pool de threads (pensez: accès à la base de données)
  • libuv a une taille de pool de threads par défaut de 4, et utilise une file d'attente pour gérer l'accès au pool de threads - le résultat est que si vous avez 5 requêtes de base de données de longue durée toutes en même temps, l'une d'elles (et tout autre asynchrone action qui repose sur le pool de threads) attendra la fin de ces requêtes avant même de commencer
  • Vous pouvez atténuer ce problème en augmentant la taille du pool de threads via la UV_THREADPOOL_SIZEvariable d'environnement, tant que vous le faites avant que le pool de threads ne soit requis et créé:process.env.UV_THREADPOOL_SIZE = 10;

Si vous voulez un multi-traitement ou un multi-threading traditionnel dans le nœud, vous pouvez l'obtenir via le clustermodule intégré ou divers autres modules tels que ceux mentionnés ci-dessus webworker-threads, ou vous pouvez le simuler en mettant en œuvre un moyen de segmenter votre travail et en utilisant manuellement setTimeoutou setImmediateou process.nextTickpour suspendre votre travail et le poursuivre dans une boucle ultérieure pour laisser les autres processus se terminer (mais ce n'est pas recommandé).

Veuillez noter que si vous écrivez du code long / bloquant en javascript, vous faites probablement une erreur. D'autres langues fonctionneront beaucoup plus efficacement.

Jason
la source
1
Putain de merde, cela clarifie complètement les choses pour moi. Merci beaucoup @Jason!
Haney
5
Pas de problème :) Je me suis retrouvé là où vous êtes il n'y a pas si longtemps, et il était difficile d'arriver à une réponse bien définie car d'un côté vous avez des développeurs C / C ++ pour qui la réponse est évidente, et de l'autre vous avez des les développeurs Web qui n'ont pas encore approfondi ce genre de questions. Je ne suis même pas sûr que ma réponse soit techniquement correcte à 100% lorsque vous descendez au niveau C, mais c'est juste dans les grandes lignes.
Jason
3
L'utilisation du pool de threads pour les requêtes réseau serait un énorme gaspillage de ressources. Selon cette question "Il effectue les E / S réseau asynchrones basées sur les interfaces d'E / S asynchrones dans différentes plates-formes, telles que epoll, kqueue et IOCP, sans pool de threads" - ce qui est logique.
Denis Dollfus
1
... cela dit, si vous faites du gros travail directement dans le thread javascript principal, ou si vous ne disposez pas de suffisamment de ressources ou ne les gérez pas de manière appropriée pour donner suffisamment de marge au pool de threads, vous pouvez introduire un décalage à une concurrence plus faible seuil - le résultat est que, pour les mêmes ressources système, vous rencontrerez généralement plus de temps avec node.js qu'avec d'autres options (bien qu'il existe d'autres systèmes basés sur des événements dans d'autres langues qui visent à contester cela - je n'ai pas vu des benchmarks récents cependant) - il est clair qu'un modèle basé sur les événements surpasse un modèle fileté.
Jason
1
@Aabid Le thread d'écoute n'exécute pas de requête de base de données, donc cela prendra environ 6 secondes pour que les 10 de ces requêtes se terminent (par la taille de pool de threads par défaut de 4). Si vous avez besoin d'effectuer un travail en javascript qui ne nécessite pas les résultats de cette requête de base de données pour se terminer, par exemple, plus de demandes arrivent qui ne nécessitent aucun travail asynchrone pour être effectué par le pool de threads, il continuera à fonctionner dans le main boucle d'événement.
Jason
20

J'ai donc une compréhension du fonctionnement de Node.js: il a un seul thread d'écoute qui reçoit un événement et le délègue ensuite à un pool de travailleurs. Le thread de travail notifie l'écouteur une fois qu'il a terminé le travail, et l'écouteur renvoie ensuite la réponse à l'appelant.

Ce n'est pas vraiment exact. Node.js n'a qu'un seul thread "de travail" qui exécute javascript. Il y a des threads dans le nœud qui gèrent le traitement des E / S, mais les considérer comme des «travailleurs» est une idée fausse. Il n'y a vraiment que la gestion des E / S et quelques autres détails sur l'implémentation interne du nœud, mais en tant que programmeur, vous ne pouvez pas influencer leur comportement à part quelques paramètres divers tels que MAX_LISTENERS.

Ma question est la suivante: si je lève un serveur HTTP dans Node.js et que j'appelle sleep sur l'un de mes événements de chemin routés (comme "/ test / sleep"), tout le système s'arrête. Même le fil d'écoute unique. Mais je crois comprendre que ce code se produit sur le pool de travailleurs.

Il n'y a pas de mécanisme de veille en JavaScript. Nous pourrions en discuter plus concrètement si vous publiez un extrait de code de ce que vous pensez que "dormir" signifie. Il n'y a pas une telle fonction à appeler pour simuler quelque chose comme time.sleep(30)en python, par exemple. Il y a setTimeoutmais ce n'est fondamentalement PAS le sommeil. setTimeoutet libérersetInterval explicitement , et non bloquer, la boucle d'événements afin que d'autres bits de code puissent s'exécuter sur le thread d'exécution principal. La seule chose que vous pouvez faire est de boucler le CPU avec un calcul en mémoire, ce qui affamera en effet le thread d'exécution principal et rendra votre programme insensible.

Comment Node.js décide-t-il d'utiliser un thread de pool de threads par rapport au thread d'écoute? Pourquoi ne puis-je pas écrire de code d'événement qui dort et ne bloque qu'un thread de pool de threads?

Le réseau IO est toujours asynchrone. Fin de l'histoire. Disk IO a à la fois des API synchrones et asynchrones, il n'y a donc pas de «décision». node.js se comportera selon les fonctions principales de l'API que vous appelez sync vs async normal. Par exemple: fs.readFilevs fs.readFileSync. Pour les processus enfants, il y a aussi séparés child_process.execet child_process.execSyncAPI.

La règle d'or est toujours d'utiliser les API asynchrones. Les raisons valables d'utiliser les API de synchronisation sont pour le code d'initialisation dans un service réseau avant qu'il n'écoute les connexions ou dans des scripts simples qui n'acceptent pas les demandes réseau pour les outils de construction et ce genre de chose.

Peter Lyons
la source
1
D'où viennent ces API asynchrones? Je comprends ce que vous dites, mais celui qui a écrit ces API a opté pour IOCP / async. Comment ont-ils choisi de faire cela?
Haney
3
Sa question est de savoir comment il écrirait son propre code chronophage sans bloquer.
Jason
1
Oui. Node fournit un réseau de base UDP, TCP et HTTP. Il fournit UNIQUEMENT des API "basées sur des pools" asynchrones. Tout le code node.js dans le monde sans exception utilise ces API asynchrones basées sur des pools car il y a tout simplement tout ce qui est disponible. Le système de fichiers et les processus enfants sont une histoire différente, mais la mise en réseau est systématiquement asynchrone.
Peter Lyons
4
Attention, Peter, de peur que vous ne soyez le pot proverbial de sa bouilloire. Il veut savoir comment les rédacteurs de l'API réseau l'ont fait, pas comment les personnes qui utilisent l'API réseau le font. J'ai finalement acquis une compréhension du comportement des nœuds par rapport aux événements non bloquants parce que je voulais écrire mon propre code non bloquant qui n'a rien à voir avec la mise en réseau ou l'une des autres API asynchrones intégrées. Il est assez clair que David veut faire de même.
Jason
2
Node n'utilise pas de pools de threads pour les E / S, il utilise des E / S non bloquantes natives, la seule exception est fs, pour autant que je sache
vkurchatkin
2

Thread pool comment quand et qui a utilisé:

Tout d'abord, lorsque nous utilisons / installons Node sur un ordinateur, il démarre un processus parmi d'autres processus qui est appelé processus de nœud dans l'ordinateur, et il continue de fonctionner jusqu'à ce que vous le tuiez. Et ce processus en cours est notre soi-disant thread unique.

entrez la description de l'image ici

Donc, le mécanisme de thread unique facilite le blocage d'une application de nœud, mais c'est l'une des fonctionnalités uniques que Node.js apporte à la table. Donc, encore une fois, si vous exécutez votre application de nœud, elle ne fonctionnera que dans un seul thread. Peu importe si vous avez 1 ou un million d'utilisateurs accédant à votre application en même temps.

Comprenons donc exactement ce qui se passe dans le thread unique de nodejs lorsque vous démarrez votre application de nœud. Au début, le programme est initialisé, puis tout le code de niveau supérieur est exécuté, ce qui signifie que tous les codes qui ne sont dans aucune fonction de rappel ( rappelez-vous que tous les codes à l'intérieur de toutes les fonctions de rappel seront exécutés sous la boucle d'événement ).

Après cela, tous les modules code exécutés puis enregistrent tous les rappels, enfin, la boucle d'événement lancée pour votre application.

entrez la description de l'image ici

Ainsi, comme nous le verrons avant, toutes les fonctions de rappel et les codes à l'intérieur de ces fonctions s'exécuteront sous une boucle d'événement. Dans la boucle d'événements, les charges sont réparties en différentes phases. Quoi qu'il en soit, je ne vais pas discuter de la boucle d'événements ici.

Eh bien, pour mieux comprendre le pool de threads, je vous demande d'imaginer que dans la boucle d'événements, les codes à l'intérieur d'une fonction de rappel s'exécutent après avoir terminé l'exécution de codes dans une autre fonction de rappel, maintenant s'il y a des tâches en fait trop lourdes. Ils bloqueraient alors notre thread unique nodejs. Et donc, c'est là que le pool de threads entre en jeu, qui est tout comme la boucle d'événements, est fourni à Node.js par la bibliothèque libuv.

Ainsi, le pool de threads ne fait pas partie de nodejs lui-même, il est fourni par libuv pour décharger de lourdes tâches sur libuv, et libuv exécutera ces codes dans ses propres threads et après l'exécution, libuv retournera les résultats à l'événement dans la boucle d'événements.

entrez la description de l'image ici

Le pool de threads nous donne quatre threads supplémentaires, ceux-ci sont complètement séparés du thread unique principal. Et nous pouvons en fait le configurer jusqu'à 128 threads.

Donc, tous ces threads forment ensemble un pool de threads. et la boucle d'événements peut alors automatiquement décharger les tâches lourdes vers le pool de threads.

La partie amusante est que tout cela se produit automatiquement dans les coulisses. Ce ne sont pas nous les développeurs qui décidons de ce qui va au pool de threads et de ce qui ne l'est pas.

Il existe de nombreuses tâches qui vont au pool de threads, telles que

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups
Seigneur
la source
0

Ce malentendu n'est que la différence entre le multitâche préventif et le multitâche coopératif ...

Le sommeil éteint tout le carnaval car il n'y a vraiment qu'une seule file d'attente pour tous les manèges et vous avez fermé la porte. Pensez-y comme "un interpréteur JS et d'autres choses" et ignorez les threads ... pour vous, il n'y a qu'un seul thread, ...

... alors ne le bloquez pas.

Gregory R. Sudderth
la source