Quand auriez-vous besoin de «centaines de milliers» de fils?


31

Erlang, Go et Rust affirment tous d'une manière ou d'une autre qu'ils prennent en charge la programmation simultanée avec des «threads» / coroutines bon marché. La FAQ Go indique:

Il est pratique de créer des centaines de milliers de goroutines dans le même espace d'adressage.

Le Tutoriel de Rust dit:

Les tâches étant beaucoup moins chères à créer que les threads traditionnels, Rust peut créer des centaines de milliers de tâches simultanées sur un système 32 bits typique.

La documentation d'Erlang dit:

La taille de tas initiale par défaut de 233 mots est assez conservatrice afin de prendre en charge les systèmes Erlang avec des centaines de milliers voire des millions de processus.

Ma question: quel type d'application nécessite autant de threads d'exécution simultanés? Seuls les serveurs Web les plus fréquentés reçoivent même des milliers de visiteurs simultanés. Les applications de type boss-worker / job-dispatching que j'ai écrites atteignent des rendements décroissants lorsque le nombre de threads / processus est bien supérieur au nombre de cœurs physiques. Je suppose que cela pourrait avoir du sens pour les applications numériques, mais en réalité, la plupart des gens délèguent le parallélisme à des bibliothèques tierces écrites en Fortran / C / C ++, pas à ces langages de nouvelle génération.


5
Je pense que la source de votre confusion est la suivante: ces microtâches / tâches / etc ne sont pas principalement destinées à remplacer les threads / processus du système d'exploitation dont vous parlez, elles ne sont pas non plus destinées à être utilisées pour diviser un gros morceau de calcul de nombres facilement parallélisable. entre quelques cœurs (comme vous l'avez correctement remarqué, il est inutile d'avoir 100k threads sur 4 cœurs à cet effet).
us2012

1
Alors à quoi servent-ils? Je suis peut-être naïf mais je n'ai jamais rencontré de situation où l'introduction de coroutines / etc aurait simplifié un programme d'exécution à un seul thread. Et j'ai pu atteindre des niveaux de concurrence "bas" avec des processus que je peux lancer sur Linux des centaines ou des milliers sans casser une sueur.
user39019

Il serait peu logique d'avoir autant de tâches qui fonctionnent réellement. Cela ne signifie pas que vous ne pouviez pas avoir un grand nombre de tâches qui étaient pour la plupart simplement bloquées en attendant que quelque chose se produise.
Loren Pechtel

5
L'idée de l'asynchronie basée sur les tâches vs asynchronie basée sur les threads est de dire que le code utilisateur doit se concentrer sur les tâches qui doivent être effectuées plutôt que de gérer les travailleurs qui effectuent ces tâches. Considérez un fil comme un travailleur que vous embauchez; embaucher un travailleur coûte cher, et si vous le faites, vous voulez qu'il travaille dur sur autant de tâches que possible 100% du temps. De nombreux systèmes peuvent être caractérisés comme ayant des centaines ou des milliers de tâches en attente, mais vous n'avez pas besoin de centaines ou de milliers de travailleurs.
Eric Lippert

Poursuivant le commentaire de @ EricLippert, il existe plusieurs situations où des centaines de milliers de tâches existeraient. Exemple # 1: la décomposition d'une tâche parallèle aux données, comme le traitement d'image. Exemple # 2: un serveur prenant en charge des centaines de milliers de clients, chacun pouvant potentiellement émettre une commande à tout moment. Chaque tâche aurait nécessité son propre «contexte d'exécution léger» - la capacité de se rappeler dans quel état elle se trouve (protocoles de communication), et la commande qu'elle exécute actuellement, et rien d'autre. Léger est possible tant que chacun a une pile d'appel peu profonde.
rwong

Réponses:


19

un cas d'utilisation - les websockets:
comme les websockets sont de longue durée par rapport aux demandes simples, sur un serveur occupé, beaucoup de websockets s'accumulent au fil du temps. les microfils vous offrent une bonne modélisation conceptuelle et une mise en œuvre relativement simple.

plus généralement, les cas dans lesquels de nombreuses unités plus ou moins autonomes attendent que certains événements se produisent devraient être de bons cas d'utilisation.


15

Il pourrait être utile de penser à ce qu'Erlang a été initialement conçu pour faire, à savoir gérer les télécommunications. Des activités comme le routage, la commutation, la collecte / agrégation de capteurs, etc.

Apporter cela dans le monde du Web - pensez à un système comme Twitter . Le système n'utiliserait probablement pas de microfilms pour générer des pages Web, mais il pourrait les utiliser dans sa collecte / mise en cache / distribution de tweets.

Cet article pourrait être d'une aide supplémentaire.


11

Dans un langage où vous n'êtes pas autorisé à modifier des variables, le simple fait de maintenir l'état nécessite un contexte d'exécution distinct (que la plupart des gens appellent un thread et Erlang appelle un processus). Fondamentalement, tout est un travailleur.

Considérez cette fonction Erlang, qui maintient un compteur:

counter(Value) ->
    receive                               % Sit idle until a message is received
        increment -> counter(Value + 1);  % Restart with incremented value
        decrement -> counter(Value - 1);  % Restart with decremented value
        speak     ->
            io:fwrite("~B~n", [Value]),
            counter(Value);               % Restart with unaltered value
        _         -> counter(Value)       % Anything else?  Do nothing.
    end.

Dans un langage OO conventionnel comme C ++ ou Java, vous accomplirez cela en ayant une classe avec un membre de classe privé, des méthodes publiques pour obtenir ou changer son état et un objet instancié pour chaque compteur. Erlang remplace la notion d'objet instancié par un processus, la notion de méthodes avec messages et le maintien de l'état avec des appels de queue qui redémarrent la fonction avec les valeurs qui composent le nouvel état. L'avantage caché de ce modèle - et la plupart de la raison d'être d' Erlang - est que le langage sérialise automatiquement l'accès à la valeur du compteur grâce à l'utilisation d'une file d'attente de messages, ce qui rend le code simultané très facile à implémenter avec un haut degré de sécurité .

Vous êtes probablement habitué à l'idée que les changements de contexte sont chers, ce qui est toujours vrai du point de vue du système d'exploitation hôte. Le runtime Erlang est lui-même un petit système d'exploitation réglé, de sorte que la commutation entre ses propres processus est rapide et efficace, tout en réduisant au minimum le nombre de changements de contexte que le système d'exploitation fait. Pour cette raison, avoir plusieurs milliers de processus n'est pas un problème et est encouragé.


1
Votre dernière application de counter/1devrait utiliser un c minuscule;) J'ai essayé de le corriger, mais StackExchange n'aime pas les modifications à 1 caractère.
d11wtq

4

Ma question: quel type d'application nécessite autant de threads d'exécution simultanés?

1) Le fait qu'une langue «évolue» signifie qu'il y a moins de chances que vous ayez à abandonner cette langue lorsque les choses deviennent plus complexes en cours de route. (C'est ce qu'on appelle le concept de «produit entier».) Beaucoup de gens abandonnent Apache pour Nginx pour cette raison. Si vous êtes proche de la «limite stricte» imposée par la surcharge des threads, vous aurez peur et commencerez à réfléchir aux moyens de la dépasser. Les sites Web ne peuvent jamais prédire le trafic qu'ils obtiendront, il est donc raisonnable de passer un peu de temps à rendre les choses évolutives.

2) Un goroutine par demande juste le début. Il existe de nombreuses raisons d'utiliser les goroutines en interne.

  • Envisagez une application web avec 100 requêtes simultanées, mais chaque requête génère des centaines de requêtes back-end. L'exemple évident est un agrégateur de moteur de recherche. Mais à peu près n'importe quelle application pourrait créer des goroutines pour chaque "zone" à l'écran, puis les générer indépendamment au lieu de séquentiellement. Par exemple, chaque page sur Amazon.com est composée de plus de 150 demandes principales, assemblées juste pour vous. Vous ne le remarquez pas car ils sont en parallèle, pas séquentiels, et chaque "zone" est son propre service web.
  • Considérez toute application où la fiabilité et la latence sont primordiales. Vous souhaitez probablement que chaque demande entrante déclenche quelques demandes principales et renvoie les données qui reviennent en premier .
  • Tenez compte de toute «jointure client» effectuée dans votre application. Au lieu de dire "pour chaque élément, obtenez des données", vous pouvez créer un tas de goroutines. Si vous avez un tas de bases de données esclaves à interroger, vous irez magiquement N fois plus vite. Sinon, ce ne sera pas plus lent.

atteindre des rendements décroissants lorsque le nombre de threads / processus est beaucoup plus élevé que le nombre de cœurs physiques

Les performances ne sont pas la seule raison de diviser un programme en CSP . Cela peut en fait rendre le programme plus facile à comprendre et certains problèmes peuvent être résolus avec beaucoup moins de code.

Comme dans les diapositives liées ci-dessus, avoir la concurrence dans votre code est un moyen d'organiser le problème. Ne pas avoir de goroutines, c'est comme ne pas avoir de structure de données Carte / Dictonary / Hash dans votre langue. Vous pouvez vous en passer. Mais une fois que vous l'avez, vous commencez à l'utiliser partout, et cela simplifie vraiment votre programme.

Dans le passé, cela signifiait «lancer votre propre» programmation multithread. Mais c'était complexe et dangereux - il n'y a toujours pas beaucoup d'outils pour s'assurer que vous ne créez pas de courses. Et comment empêcher un futur responsable de faire une erreur? Si vous regardez des programmes grands / complexes, vous verrez qu'ils dépensent BEAUCOUP de ressources dans cette direction.

Étant donné que la concurrence n'est pas une partie de première classe de la plupart des langues, les programmeurs d'aujourd'hui ont un angle mort pour savoir pourquoi cela leur serait utile. Cela ne fera que devenir plus évident que chaque téléphone et montre-bracelet se dirige vers 1000 cœurs. Partez avec un outil de détection de course intégré.


2

Pour Erlang, il est courant d'avoir un processus par connexion ou autre tâche. Ainsi, par exemple, un serveur de streaming audio peut avoir 1 processus par utilisateur connecté.

La machine virtuelle Erlang est optimisée pour gérer des milliers voire des centaines de milliers de processus en rendant les changements de contexte très bon marché.


1

Commodité. À l'époque où j'ai commencé à faire de la programmation multi-thread, je faisais beaucoup de simulation et de développement de jeux à côté pour le plaisir. J'ai trouvé qu'il était très pratique de simplement dériver un fil pour chaque objet et de le laisser faire sa propre chose plutôt que de traiter chacun par une boucle. Si votre code n'est pas perturbé par un comportement non déterministe et que vous n'avez pas de collisions, cela peut faciliter le codage. Avec la puissance dont nous disposons maintenant, si je devais y revenir, je peux facilement imaginer tourner quelques milliers de threads en raison d'avoir suffisamment de puissance de traitement et de mémoire pour gérer autant d'objets discrets!


1

Un exemple simple pour Erlang, qui a été conçu pour la communication: le transfert de paquets réseau. Lorsque vous effectuez une seule requête http, vous pouvez avoir des milliers de paquets TCP / IP. Ajoutez à cela que tout le monde se connecte en même temps, et vous avez votre cas d'utilisation.

Considérez de nombreuses applications utilisées en interne par toute grande entreprise pour gérer leurs commandes ou tout ce dont elles pourraient avoir besoin. Les serveurs Web ne sont pas les seuls à avoir besoin de threads.


-2

Certaines tâches de rendu me viennent à l'esprit ici. Si vous effectuez une longue chaîne d'opérations sur chaque pixel d'une image, et si ces opérations sont parallélisables, alors même une image relativement petite de 1024 x 768 se situe dans la tranche des "centaines de milliers".


2
Il y a quelques années, j'ai passé quelques années à faire du traitement d'images FLIR en temps réel, à croquer des images 256x256 à 30 images par seconde. À moins que vous ayez BEAUCOUP de processeurs HARDWARE et une manière SEAMLESS de partitionner vos données entre eux, la DERNIÈRE chose que vous voulez faire est d'ajouter le changement de contexte, les conflits de mémoire et le cache de cache aux coûts de calcul réels.
John R. Strohm

Cela dépend du travail effectué. Si tout ce que vous faites est de transférer un travail à une unité matérielle / d'exécution, après quoi vous pouvez effectivement l'oublier (et notez que c'est la façon dont les GPU fonctionnent, donc ce n'est pas un scénario hypothétique), alors l'approche est valide.
Maximus Minimus
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.