Question

La programmation asynchrone semble devenir très populaire ces jours-ci.L'un des avantages les plus cités est le gain de performances résultant de la suppression des opérations qui bloquent les threads.Mais j'ai aussi vu des gens dire que l'avantage n'est pas si grand.Et ce pool de threads correctement configuré peut avoir le même effet qu'un code entièrement asynchrone.

Ma question est la suivante : existe-t-il de véritables références qui comparent le code de blocage au code asynchrone ?Cela peut être n'importe quel langage, mais je pense que C# et Java seraient les plus représentatifs.Je ne sais pas à quel point ce benchmark serait bon avec Node.js ou similaire.

Edit : Ma tentative de question générale combinée à une terminologie peu claire semble avoir échoué.Par "code asynchrone", j'entends ce que certaines réponses décrivent comme une programmation d'événement ou de rappel.Dans le résultat final, les opérations qui bloqueraient le thread sont plutôt déléguées à un système de rappel afin que les threads puissent être mieux utilisés.

Et si je voulais poser une question spécifique : existe-t-il des benchmarks qui comparent le gain de débit/latence du code serveur async/wait dans .NET ?Ou une autre comparaison similaire?

Était-ce utile?

La solution

D'après vos commentaires, il semble que vous soyez vraiment intéressé par les "E/S non bloquantes".Cela diffère de ma définition de la "programmation asynchrone", qui est une approche de la décomposition du travail illustrée par les processus Erlang ou Goroutines.

Et, si c'est votre définition, alors oui, il y en a eu repères. Mais, comme tous les repères, ils ne devraient pas être acceptés aveuglément.Au lieu de cela, vous devez penser à ce qui se passe dans les coulisses.

  • Un thread est l'unité de planification du système d'exploitation.Des plates-formes telles qu'Erlang et Go créent leurs propres planificateurs au-dessus du planificateur du système d'exploitation, permettant à plusieurs unités d'exécution de partager le même thread.C'est génial, tant que vos unités d'exécution sont légères, car cela évite les frais généraux associés aux threads.* Cependant, les opérations d'E/S nécessitent un voyage vers le noyau, ce qui signifie que vous avez besoin d'un vrai thread pour les faire.Et si vous implémentez un planificateur de sous-thread, vous devez faire attention à ne pas planifier de tâches de sous-thread sur un thread bloqué dans une opération du noyau.
  • Toutes les opérations d'E/S ont le potentiel de bloquer.** Lorsque vous faites une requête read ou write, le noyau regarde s'il y a des données disponibles ou (pour l'écriture) de la place dans un tampon.Sinon, le noyau suspend le thread jusqu'à ce que l'opération puisse se terminer.Cela rend les serveurs thread par connexion très simples à implémenter, mais inquiète les personnes qui pensent aux surcharges de thread.
  • Les systèmes d'exploitation permettent de bloquer simultanément plusieurs canaux d'E/S.L'appel select sur POSIX est l'un de ceux-ci : vous lui fournissez une liste de canaux (descripteurs de fichiers / sockets) qui vous intéressent, et il vous dira quand l'un d'eux est prêt à lire ou à écrire (lire est ce que la plupart des gensse soucier).Vous devez toujours faire un appel au noyau, et vous finirez toujours par bloquer un thread si rien n'est disponible, mais ce n'est qu'un thread.C'est comment fonctionne Node.js : lorsque les données sont disponibles, le gestionnaire d'événements approprié est appelé (je ne connais pas les composants internes de Node, mais j'espère qu'ils vérifient également que les tampons d'écriture sont disponibles avant d'appeler write).
  • Lorsque vous maximisez le CPU, vous avez terminé.Peu importe que vous utilisiez select ou une approche de thread par connexion, vous devez toujours dépenser du CPU pour faire tout ce que votre serveur est censé faire.Avec l'approche thread par connexion, vous ne faites pas vraiment attention à cela : le planificateur affectera les threads aux cœurs, et vous vous dégraderez gracieusement.Avec select, vous devrez transférer les connexions aux threads lorsqu'ils seront prêts à être traités, ou vous serez limité par les performances d'un seul cœur (Node.js contourne cela en vous permettant de générer plusieurs serveurs).

Comme je l'ai dit, les repères ne devraient pas être acceptés aveuglément;ils ne sont valables que tant qu'ils modélisent le problème du monde réel que vous essayez de résoudre.L'auteur du benchmark lié travaille pour (a travaillé pour ?) Mailinator, qui si vous ne l'avez pas utilisé, est un service poste restante pour les adresses e-mail ad hoc.Ce qui signifie qu'il va obtenir des connexions de courte durée et de haute activité à partir d'un nombre relativement restreint de clients.Il s'agit d'un cas d'utilisation parfait pour la planification de threads par connexion.Comme indiqué dans les commentaires, un serveur de discussion (longue durée de vie, faible activité) peut être différent.

Dans mon esprit, la question des E/S bloquantes et non bloquantes est plutôt ennuyeuse : la plupart des serveurs du monde réel n'ont pas autant de connexions simultanées.Plus intéressant pour moi est le modèle de programmation : un modèle basé sur les travailleurs comme Erlang ou Go signifie que vous pouvez vous concentrer sur votre logique métier, sans vous soucier de la façon dont les connexions sont gérées.

* Ces frais généraux incluent les structures de planification du noyau, et peut-être le plus important, une pile de threads de plusieurs mégaoctets, dont la plupart restent inutilisés.Bien que 2 Mo ne semblent pas beaucoup, cela s'additionne rapidement si vous avez 100 000 processus ... ce que la plupart des applications n'ont pas.

** Ce n'est pas vrai à 100%, mais je ne veux pas entrer trop profondément dans les mauvaises herbes ici.

Autres conseils

Toute la programmation asynchrone (dans les nœuds et autres implémentations asynchrones à thread unique) consiste à masquer les latences en pouvant continuer à travailler pendant que vous attendez des ressources externes et en pouvant demander plusieurs ressources externes en même temps afin que leurs latences se chevauchent.Il ne devrait y avoir aucune différence entre async et threads si l'utilisation de threads est limitée à l'attente de ressources et que vous ne surchargez pas le système avec des threads.

Certaines implémentations asynchrones exécutent les tâches asynchrones sur un pool de threads. Pour ces implémentations, je ne m'attendrais pas à ce qu'il y ait une différence autre que la différence de surcharge de bibliothèque et d'intelligence de bibliothèque dans la gestion des threads.

http://www.ducons.com/blog/tests-and-thoughts-on-asynchronous-io-vs-multithreading

Lorsque les gens font référence aux avantages en termes de performances du code asynchrone par rapport au code bloquant, ils parlent dans le contexte d'un seul thread.Si vous vous arrêtez pendant 2 secondes et ne faites rien pendant le téléchargement d'une page Web, c'est évidemment plus lent que de continuer à faire d'autres choses pendant que vous attendez.

Oui, les pools de threads peuvent accomplir le même effet.La différence est que le code asynchrone est généralement plus facile à écrire correctement par rapport au code multithread, et le code asynchrone n'a généralement pas besoin d'un thread pour le soutenir, il est donc plus léger en ressources.

Par exemple, l'attente de 100 appels réseau ne nécessite que 100 descripteurs de socket, au lieu de 100 descripteurs de socket et 100 threads.Cela signifie que les références telles que la latence pour le téléchargement d'une page Web seront à peu près les mêmes, mais le code asynchrone aura généralement des avantages dans des choses comme le nombre de sockets simultanés.Le type de benchmark que vous utilisez dépend de votre cas d'utilisation.

Pour comparer le code asynchrone, vous mesurez ses performances de la manière habituelle et le comparez à son équivalent non synchrone.

Time for Synchronous call to return to caller: 900 ms
Time for asynchronous call to return to caller: 50 ms
Time saved: 850 ms.

Bien sûr, vous n'obtenez pas ce gain de temps gratuitement ;le thread résultant qui est dérivé de l'appel asynchrone doit encore passer 850 ms en arrière-plan pour terminer son travail, et si vous lancez suffisamment de threads, vous finirez par manquer de cœurs (sur les tâches liées au processeur), donc votre kilométrage peut varier.

Pour connaître l'amélioration globale que vous obtiendrez, vous pouvez exécuter un essai de charge.

En fait, les deux approches ont leur lot d'inconvénients.C'est un sujet plutôt compliqué qui va bien au-delà des discussions contre les événements.

Je vous recommande de lire l'excellent "Programmation concurrente pour les architectures Web évolutives", soit le tout (ce que je recommande) ou au moins les sections 4.2 Architectures de serveur et 4.3 Cas de threads vs événements.

Quelle que soit la façon dont vous définissez la programmation asynchrone , il n'y a pas de réponse globale.Certains problèmes de programmation sont résolus plus efficacement avec des threads (ou "programmation asynchrone"), d'autres subiront simplement un impact inutile sur les performances en créant un nouveau thread (ou en envoyant un travail à un thread).Lorsque vous rencontrez un problème particulier avec deux solutions, il vous suffit de comparer les performances - du début à la fin - de chaque solution potentielle.

Quelques indicateurs qu'un travail peut bénéficier d'une solution threadée/asynchrone :

  • Il existe plusieurs calculs potentiellement longs qui peuvent être effectués indépendamment les uns des autres
  • Il y a des récupérations/mises de données « distantes » (via HTTP, par exemple) aux côtés d'autres codes qui ne dépendent pas des données
  • Il existe de nombreuses récupérations de données, interactions ou calculs de moyenne à longue durée qui peuvent être exécutés indépendamment les uns des autres

Ou plus fondamentalement, si vous atteignez un point dans le code où vous avez besoin de doSomething() et que vous savez que

  1. doSomething() prendra un "long" temps, et ...
  2. Vous pouvez continuer à faire d'autres choses urgentes, importantes et/ou chronophages pendant que vous attendez doSomething(), puis...

... il est probablement avantageux d'exécuter doSomething() de manière asynchrone.(Mais, vous devez toujours comparer, même si "de manière informelle", pour en être sûr!) Que vous soyez explicitement conscient que doSomething() a son propre thread ou simplement que vous avez appelé doSomethingAsync() à la place dépend de vous et/ou de votre framework...

Licencié sous: CC-BY-SA avec attribution
scroll top