web-dev-qa-db-fra.com

Les E/S non bloquantes sont-elles vraiment plus rapides que les E/S bloquantes multi-thread? Comment?

J'ai cherché sur le Web des informations techniques sur le blocage des E/S et les E/S non bloquantes et j'ai trouvé plusieurs personnes affirmant que des E/S non bloquantes seraient plus rapides que des E/S bloquées. Par exemple, dans ce document .

Si j'utilise le blocage d'E/S, alors bien sûr le thread actuellement bloqué ne peut rien faire d'autre ... Parce qu'il est bloqué. Mais dès qu'un thread commence à être bloqué, le système d'exploitation peut passer à un autre thread et ne pas revenir en arrière tant qu'il n'y a rien à faire pour le thread bloqué. Donc, tant qu’un autre thread sur le système a besoin de la CPU et n’est pas bloqué, il ne devrait plus y avoir de temps mort de la CPU par rapport à une approche non bloquante basée sur les événements, n’est-ce pas?

En plus de réduire le temps pendant lequel le processeur est inactif, je vois une option supplémentaire pour augmenter le nombre de tâches qu'un ordinateur peut effectuer dans un laps de temps donné: Réduisez le temps système introduit par le changement de threads. Mais comment cela peut-il se faire? Et les frais généraux sont-ils suffisamment importants pour produire des effets mesurables? Voici une idée sur la façon dont je peux imaginer que cela fonctionne:

  1. Pour charger le contenu d'un fichier, une application délègue cette tâche à une infrastructure d'E/S basée sur des événements, en transmettant une fonction de rappel avec un nom de fichier.
  2. La structure des événements délègue au système d'exploitation le programme qui commande à un contrôleur DMA du disque dur d'écrire le fichier directement en mémoire.
  3. La structure d'événement permet l'exécution ultérieure de code.
  4. Une fois la copie de disque à mémoire terminée, le contrôleur DMA provoque une interruption.
  5. Le gestionnaire d'interruptions du système d'exploitation informe le framework d'e/s basé sur les événements du chargement complet du fichier en mémoire. Comment ça fait ça? Utiliser un signal ??
  6. Le code actuellement exécuté dans le cadre de l'événement e/s se termine.
  7. L'infrastructure d'E/S basée sur les événements vérifie sa file d'attente et voit le message du système d'exploitation de l'étape 5 et exécute le rappel reçu à l'étape 1.

Est-ce que ça fonctionne? Si ça ne fonctionne pas, comment ça marche? Cela signifie que le système d’événements peut fonctionner sans jamais avoir besoin de toucher explicitement la pile (comme un vrai planificateur qui aurait besoin de sauvegarder la pile et de copier la pile d’un autre thread en mémoire lors du changement de thread). Combien de temps cela économise-t-il réellement? Y at-il plus à cela?

101
yankee

Le principal avantage des E/S non bloquantes ou asynchrones est que votre thread peut continuer son travail en parallèle. Bien sûr, vous pouvez y parvenir également en utilisant un thread supplémentaire. Comme vous l'avez indiqué pour obtenir les meilleures performances (système) globales, il serait préférable d'utiliser une E/S asynchrone et non plusieurs threads (afin de réduire la commutation de threads).

Regardons les implémentations possibles d'un programme de serveur de réseau devant gérer 1000 clients connectés en parallèle:

  1. Un thread par connexion (peut bloquer des E/S, mais peut aussi être des E/S non bloquantes).
    Chaque thread nécessite des ressources de mémoire (également la mémoire du noyau!), Ce qui est un inconvénient. Et chaque fil supplémentaire signifie plus de travail pour le planificateur.
  2. Un thread pour toutes les connexions.
    Cela prend la charge du système car nous avons moins de threads. Mais cela vous empêche également d’utiliser pleinement les performances de votre ordinateur, car vous risqueriez de conduire un processeur à 100% et de laisser tous les autres processeurs inactifs.
  3. Quelques threads où chaque thread gère certaines des connexions.
    Cela prend la charge du système car il y a moins de threads. Et il peut utiliser tous les processeurs disponibles. Sous Windows, cette approche est prise en charge par API de pool de threads .

Bien sûr, avoir plus de threads n'est pas un problème en soi. Comme vous l'avez peut-être reconnu, j'ai choisi un nombre assez élevé de connexions/threads. Je doute que vous constatiez une différence entre les trois implémentations possibles si nous ne parlons que d'une douzaine de threads (c'est également ce que suggère Raymond Chen dans l'article de blog MSDN Windows a-t-il une limite de 2 000 threads par processus? ).

Sous Windows, avec fichier non mis en tampon, I/O signifie que les écritures doivent avoir une taille qui est un multiple de la taille de la page. Je ne l'ai pas testée, mais il semblerait que cela pourrait également affecter positivement les performances d'écriture pour les écritures synchrones et asynchrones mises en tampon.

Les étapes 1 à 7 que vous décrivez donnent une bonne idée de son fonctionnement. Sous Windows, le système d'exploitation vous informera de l'achèvement d'une E/S asynchrone (WriteFile avec une structure OVERLAPPED) à l'aide d'un événement ou d'un rappel. Les fonctions de rappel ne seront appelées, par exemple, que lorsque votre code appelle WaitForMultipleObjectsEx avec bAlertable défini sur true.

Quelques lectures supplémentaires sur le web:

37
Werner Henze

Les E/S incluent plusieurs types d'opérations telles que la lecture et l'écriture de données à partir de disques durs, l'accès aux ressources réseau, l'appel de services Web ou la récupération de données à partir de bases de données. Selon la plate-forme et le type d'opération, les E/S asynchrones tirent généralement parti de tout support matériel ou système de niveau bas pour effectuer l'opération. Cela signifie qu’elle sera exécutée avec le moins d’impact possible sur le processeur.

Au niveau de l'application, les E/S asynchrones évitent aux threads d'attendre la fin des opérations d'E/S. Dès qu'une opération d'E/S asynchrone est démarrée, le thread sur lequel elle a été lancée est libéré et un rappel est enregistré. Lorsque l'opération est terminée, le rappel est mis en file d'attente pour être exécuté sur le premier thread disponible.

Si l'opération d'E/S est exécutée de manière synchrone, le thread en cours d'exécution reste actif jusqu'à la fin de l'opération. Le moteur d'exécution ne sait pas quand l'opération d'E/S est terminée. Par conséquent, il fournira périodiquement du temps CPU au thread en attente, un temps CPU qui aurait autrement pu être utilisé par d'autres threads ayant des opérations liées au CPU à effectuer.

Ainsi, comme @ user1629468 l'a mentionné, les E/S asynchrones n'offrent pas de meilleures performances, mais une meilleure évolutivité. Cela est évident lors de l'exécution dans des contextes disposant d'un nombre limité de threads, comme c'est le cas avec les applications Web. Les applications Web utilisent généralement un pool de threads à partir duquel elles attribuent des threads à chaque demande. Si les demandes sont bloquées lors d'opérations d'E/S de longue durée, il existe un risque d'épuisement du pool Web et de blocage ou de ralentissement de l'application Web.

Une chose que j'ai remarquée est que les E/S asynchrones ne sont pas la meilleure option pour traiter des opérations d'E/S très rapides. Dans ce cas, l'avantage de ne pas garder un thread occupé pendant l'attente de la fin de l'opération d'E/S n'est pas très important et le fait que l'opération est démarrée sur un thread et qu'elle est terminée sur un autre ajoute une surcharge à l'exécution globale.

Vous pouvez lire une recherche plus détaillée que j'ai faite récemment sur le sujet des entrées/sorties asynchrones et du multithreading ici .

26

La principale raison d'utiliser AIO est l'évolutivité. Vus dans le contexte de quelques threads, les avantages ne sont pas évidents. Mais lorsque le système atteindra des milliers de threads, AIO offrira de bien meilleures performances. La mise en garde est que la bibliothèque AIO ne doit pas introduire d'autres goulots d'étranglement.

4
fissurezone

Pour présumer d'une amélioration de la vitesse due à toute forme de multi-informatique, vous devez présumer que plusieurs tâches à base de CPU sont exécutées simultanément sur plusieurs ressources informatiques (généralement des cœurs de processeur), ou que toutes les tâches ne reposent pas sur l'utilisation simultanée de la même ressource, c’est-à-dire que certaines tâches peuvent dépendre d’un sous-composant du système (stockage sur disque, par exemple), tandis que certaines tâches en dépendent d’un autre (recevoir les communications d’un périphérique) et que d’autres peuvent nécessiter l’utilisation de cœurs de processeur.

Le premier scénario est souvent appelé programmation "parallèle". Le second scénario est souvent appelé programmation "concurrente" ou "asynchrone", bien que "concurrente" soit parfois aussi utilisé pour désigner le cas de la simple possibilité pour un système d'exploitation d'entrelacer l'exécution de plusieurs tâches, que cette exécution soit ou non exécutée. place en série ou si plusieurs ressources peuvent être utilisées pour réaliser une exécution parallèle. Dans ce dernier cas, "simultané" fait généralement référence à la manière dont l'exécution est écrite dans le programme, plutôt que du point de vue de la simultanéité réelle de l'exécution de la tâche.

Il est très facile de parler de tout cela avec des hypothèses tacites. Par exemple, certains déclarent rapidement que "les E/S asynchrones seront plus rapides que les E/S multi-threadées". Cette affirmation est douteuse pour plusieurs raisons. Premièrement, il est possible que certaines infrastructures d’E/S asynchrones soient implémentées précisément avec le multi-threading, auquel cas elles sont identiques et il n’est pas logique de dire qu’un concept est "plus rapide que l’autre" . 

Deuxièmement, même dans le cas où il existe une implémentation mono-thread d'un framework asynchrone (telle qu'une boucle d'événement mono-thread), vous devez toujours faire une hypothèse sur ce que fait cette boucle. Par exemple, une chose idiote que vous pouvez faire avec une boucle d’événement à un seul thread est de lui demander de terminer de manière asynchrone deux tâches différentes purement liées au processeur. Si vous exécutiez cette tâche sur une machine ne disposant que d'un seul processeur idéalisé (en ignorant les optimisations matérielles modernes), effectuer cette tâche "de manière asynchrone" ne serait pas vraiment différent de l'exécution avec deux threads gérés indépendamment ou avec un seul processus isolé - - la différence peut venir de l'optimisation de la commutation de contexte de thread ou du système d'exploitation, mais si les deux tâches sont transférées à la CPU, elles seraient similaires dans les deux cas.

Il est utile d’imaginer un grand nombre de cas inhabituels ou stupides que vous pourriez rencontrer.

"Asynchrone" ne doit pas nécessairement être simultané, par exemple comme ci-dessus: vous exécutez "de manière asynchrone" deux tâches liées à la CPU sur une machine avec exactement un coeur de processeur.

L'exécution multi-thread ne doit pas nécessairement être simultanée: vous créez deux threads sur une machine avec un seul cœur de processeur ou vous demandez à deux threads d'acquérir tout autre type de ressource rare (imaginons, par exemple, une base de données réseau qui ne peut en établir qu'un seul). connexion à la fois). L'exécution des threads peut être interleaved. Cependant, le planificateur du système d'exploitation le souhaite, mais leur temps d'exécution total ne peut pas être réduit (et sera augmenté à partir du changement de contexte du thread) sur un seul cœur les threads qu'il n'y a de cœurs pour les exécuter, ou ont plus de threads demandant une ressource que ce que la ressource peut supporter). Cette même chose vaut aussi pour le multi-traitement.

Ainsi, ni les E/S asynchrones ni le multithreading ne doivent offrir de gain de performances en termes de temps d'exécution. Ils peuvent même ralentir les choses.

Toutefois, si vous définissez un cas d'utilisation spécifique, par exemple un programme spécifique qui effectue un appel réseau pour extraire des données d'une ressource connectée au réseau, telle qu'une base de données distante, et effectue également des calculs locaux liés à l'UC, vous pouvez alors commencer à raisonner. les différences de performance entre les deux méthodes étant donné une hypothèse particulière sur le matériel.Les questions à poser: combien d’étapes de calcul dois-je exécuter et combien de systèmes de ressources indépendants existe-t-il pour les exécuter? Existe-t-il des sous-ensembles d’étapes de calcul nécessitant l’utilisation de sous-composants système indépendants et pouvant en tirer profit simultanément? Combien de cœurs de processeur ai-je et quelle est la surcharge liée à l'utilisation de plusieurs processeurs ou threads pour effectuer des tâches sur des cœurs séparés?.

Si vos tâches reposent en grande partie sur des sous-systèmes indépendants, une solution asynchrone peut s'avérer judicieuse. Si le nombre de threads nécessaires pour le gérer était important, de sorte que la commutation de contexte devienne non triviale pour le système d'exploitation, une solution asynchrone à un seul thread pourrait être préférable.

Lorsque les tâches sont liées par la même ressource (par exemple, plusieurs besoins pour accéder simultanément au même réseau ou à la même ressource locale), le multi-threading introduira probablement un surcoût insatisfaisant. Une telle situation de ressources limitées ne peut pas non plus accélérer. Dans ce cas, la seule option (si vous souhaitez accélérer le processus) consiste à créer plusieurs copies de cette ressource (par exemple, plusieurs cœurs de processeur si la ressource rare est CPU; une meilleure base de données prenant en charge davantage de connexions simultanées si la ressource rare est une base de données limitée par la connexion, etc.). 

Une autre façon de le dire est la suivante: permettre au système d'exploitation d'entrelacer l'utilisation d'une ressource unique pour deux tâches ne peut pas être plus rapide que de simplement laisser une tâche utiliser la ressource pendant que l'autre attend, puis laisser la deuxième tâche se terminer en série. De plus, le coût d'entrelacement du planificateur signifie que, dans toute situation réelle, il en résulte un ralentissement. Peu importe que l'utilisation entrelacée se produise de la part de la CPU, d'une ressource réseau, d'une ressource mémoire, d'un périphérique ou de toute autre ressource système.

Another way to put it is: allowing the operating system to interleave the usage of a single resource for two tasks cannot be faster than merely letting one task use the resource while the other waits, then letting the second task finish serially. Further, the scheduler cost of interleaving means in any real situation it actually creates a slowdown. It doesn't matter if the interleaved usage occurs of the CPU, a network resource, a memory resource, a peripheral device, or any other system resource.

3
ely

Une implémentation possible d'E/S non bloquantes correspond exactement à ce que vous avez dit, avec un groupe de threads d'arrière-plan qui bloquent les E/S et notifient le thread de l'expéditeur des E/S via un mécanisme de rappel. En fait, c'est ainsi que fonctionne le module AIO de glibc. Voici quelques détails vagues sur la mise en œuvre.

Bien qu'il s'agisse d'une bonne solution assez portable (tant que vous avez des threads), le système d'exploitation est généralement capable de gérer les E/S non bloquantes plus efficacement. Cet article Wikipedia répertorie les implémentations possibles en plus du pool de threads.

2
Miguel

Je suis actuellement en train de mettre en œuvre async io sur une plate-forme intégrée utilisant des protothreads. Io non bloquant fait la différence entre courir à 16000fps et 160fps. Le principal avantage de l'io non bloquant est que vous pouvez structurer votre code pour qu'il fasse autre chose tandis que le matériel le fait. Même l'initialisation des périphériques peut être faite en parallèle. 

Martin

2
user2826084

Pour autant que je sache, l'amélioration réside dans le fait que les E/S asynchrones utilisent (je parle de MS System, juste pour clarifier) ​​les soi-disant ports de complétion des E/S . En utilisant l'appel asynchrone, la structure exploite automatiquement cette architecture, ce qui est censé être beaucoup plus efficace que le mécanisme de threading standard. En tant qu'expérience personnelle, je peux dire que vous sentiriez sensiblement votre application plus réactive si vous préférez AsyncCalls au lieu de bloquer les threads.

0
Felice Pollano

Dans Node, plusieurs threads sont en cours de lancement, mais il s’agit d’une couche inférieure lors de l’exécution C++. 

"Donc, oui, NodeJS est à thread unique, mais c’est une demi-vérité. C’est en fait un événement basé sur des événements et un seul thread avec des travailleurs d’arrière-plan. La boucle d’événement principale est à thread unique, mais la plupart des travaux d’E/S sont exécutés sur des threads séparés parce que les API d'E/S dans Node.js sont asynchrones/non bloquantes de par leur conception, afin de prendre en charge la boucle d'événement. "

https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea

"Node.js est non bloquant, ce qui signifie que toutes les fonctions (callbacks) sont déléguées à la boucle d'événements et qu'elles sont (ou peuvent être) exécutées par différents threads. Cela est géré par l'exécution de Node.js."

https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98

L'explication "Le nœud est plus rapide parce que c'est non bloquant ..." est un peu de marketing et c'est une excellente question. Il est efficace et évolutif, mais pas exactement à thread unique.

0
SmokestackLightning