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

119

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

Si j'utilise le blocage des 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 basculer vers un autre thread et ne pas revenir en arrière jusqu'à ce qu'il y ait quelque chose à faire pour le thread bloqué. Donc, tant qu'il y a un autre thread sur le système qui a besoin de CPU et qui n'est pas bloqué, il ne devrait plus y avoir de temps d'inactivité du CPU par rapport à une approche non bloquante basée sur les événements, n'est-ce pas?

En plus de réduire le temps d'inactivité du processeur, je vois une autre option pour augmenter le nombre de tâches qu'un ordinateur peut effectuer dans un laps de temps donné: réduire la surcharge introduite par le changement de thread. Mais comment cela peut-il se faire? Et les frais généraux sont-ils suffisamment importants pour montrer des effets mesurables? Voici une idée sur la façon dont je peux l'imaginer fonctionner:

  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 passant une fonction de rappel avec un nom de fichier
  2. L'infrastructure d'événements délègue au système d'exploitation, qui programme un contrôleur DMA du disque dur pour écrire le fichier directement dans la mémoire
  3. Le cadre d'événements permet l'exécution de code supplémentaire.
  4. Une fois la copie disque vers mémoire terminée, le contrôleur DMA provoque une interruption.
  5. Le gestionnaire d'interruptions du système d'exploitation informe la structure d'E / S basée sur les événements que le fichier est complètement chargé en mémoire. Comment fait-il cela? Utilisation d'un signal ??
  6. Le code qui est actuellement exécuté dans le cadre d'E / S d'événements 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 à partir de l'étape 5 et exécute le rappel obtenu à l'étape 1.

Est-ce ainsi que cela fonctionne? Si ce n'est pas le cas, 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 tout en changeant de thread)? Combien de temps cela fait-il réellement gagner? Y a-t-il autre chose?

Yankee
la source
5
réponse courte: il s'agit plus de la surcharge d'avoir un thread par connexion. non bloquant io permet d'éviter d'avoir un thread par connexion.
Dan D.
10
Le blocage des E / S est coûteux sur un système où vous ne pouvez pas créer autant de threads qu'il existe de connexions. Sur la JVM, vous pouvez créer quelques milliers de threads, mais que faire si vous avez plus de 100 000 connexions? Il faut donc s'en tenir à une solution asynchrone. Cependant, il existe des langages où les threads ne sont pas chers (par exemple les threads verts) comme dans Go / Erlang / Rust où ce n'est pas un problème d'avoir 100.000 threads. Lorsque le nombre de threads peut être important, je pense que le blocage des E / S donne des temps de réponse plus rapides. Mais c'est quelque chose que je devrais également demander aux experts si cela est vrai dans la réalité.
OlliP
@OliverPlow, je le pense aussi, car le blocage des E / S signifie généralement que nous laissons le système gérer la "gestion parallèle", au lieu de le faire nous-mêmes en utilisant des files d'attente de tâches et autres.
Pacerier
1
@DanD., Et si la surcharge d'avoir des threads est égale à la surcharge de l'exécution des E / S non bloquantes? (généralement vrai dans le cas des fils verts)
Pacerier
"copier la pile" ne se produit pas. Différents threads ont leurs piles à des adresses différentes. Chaque thread a son propre pointeur de pile, ainsi que d'autres registres. Un changement de contexte enregistre / restaure uniquement l'état architectural (y compris tous les registres), mais pas la mémoire. Entre les threads dans le même processus, le noyau n'a même pas à changer les tables de pages.
Peter Cordes

Réponses:

44

Le plus grand avantage des E / S non bloquantes ou asynchrones est que votre thread peut continuer son travail en parallèle. Bien sûr, vous pouvez également y parvenir en utilisant un fil supplémentaire. Comme vous l'avez déclaré pour obtenir les meilleures performances globales (système), je suppose qu'il serait préférable d'utiliser des E / S asynchrones et non plusieurs threads (réduisant ainsi le changement de thread).

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

  1. Un thread par connexion (peut être des E / S bloquantes, mais peut également être des E / S non bloquantes).
    Chaque thread nécessite des ressources de mémoire (également de la mémoire du noyau!), C'est un inconvénient. Et chaque thread supplémentaire signifie plus de travail pour le planificateur.
  2. Un fil 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 toutes les performances de votre machine, car vous pourriez finir par conduire un processeur à 100% et 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 l' API Thread Pool .

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 voyiez une différence entre les trois implémentations possibles si nous ne parlons que d'une dizaine de threads (c'est aussi ce que suggère Raymond Chen sur le billet de blog MSDN. Windows a-t-il une limite de 2000 threads par processus? ).

Sous Windows, l'utilisation d' E / S de fichier sans tampon signifie que les écritures doivent être d'une taille qui est un multiple de la taille de la page. Je ne l'ai pas testé, mais il semble que cela pourrait également affecter positivement les performances d'écriture pour les écritures synchrones et asynchrones tamponnées.

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 ( WriteFileavec OVERLAPPEDstructure) à 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 WaitForMultipleObjectsExavec bAlertableréglé sur true.

Quelques lectures supplémentaires sur le Web:

Werner Henze
la source
Du point de vue du Web, les connaissances courantes (Internet, commentaires d'experts) suggèrent que l'augmentation considérable du max. Le nombre de threads de requête est une mauvaise chose pour bloquer les E / S (rendant le traitement des requêtes encore plus lent) en raison de l'augmentation de la mémoire et du temps de changement de contexte, mais Async IO ne fait-il pas la même chose lors du report du travail à un autre thread? Oui, vous pouvez répondre à plus de demandes maintenant mais avoir le même nombre de threads en arrière-plan ... quel est le véritable avantage de cela?
JavierJ
1
@JavierJ Vous semblez croire que si n threads font un fichier asynchrone IO, un autre n threads sera créé pour faire un fichier bloquant IO? Ce n'est pas vrai. Le système d'exploitation prend en charge les E / S de fichiers asynchrones et il n'a pas besoin de se bloquer lors de l'attente de la fin des E / S. Il peut mettre en file d'attente les demandes d'E / S et si une interruption matérielle (par exemple DMA) se produit, il peut marquer la demande comme terminée et définir un événement qui signale le thread des appelants. Même si un thread supplémentaire était requis, le système d'exploitation pourrait utiliser ce thread pour plusieurs demandes d'E / S provenant de plusieurs threads.
Werner Henze
Merci, cela a du sens d'impliquer le support IO du fichier asynchrone du système d'exploitation, mais lorsque j'écris du code pour une implémentation réelle de ceci (du point de vue Web), disons avec Java Servlet 3.0 NIO, je vois toujours un fil pour la demande et un fil d'arrière-plan ( async) en boucle pour lire un fichier, une base de données ou autre.
JavierJ
1
@piyushGoyal J'ai réécrit ma réponse. J'espère que c'est plus clair maintenant.
Werner Henze
1
Sous Windows, l'utilisation d'E / S de fichiers asynchrones signifie que les écritures doivent être d'une taille qui est un multiple de la taille de la page. - non, ce n'est pas le cas. Vous pensez à des E / S sans tampon. (Ils sont souvent utilisés ensemble, mais ils ne doivent pas nécessairement l'être.)
Harry Johnston
29

Les E / S comprennent 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. En fonction de la plate-forme et du type d'opération, les E / S asynchrones tireront généralement parti de tout support matériel ou système de bas niveau pour effectuer l'opération. Cela signifie qu'il sera exécuté 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 lancée, elle libère le thread sur lequel elle a été lancée et un rappel est enregistré. Une fois l'opération terminée, le rappel est mis en file d'attente pour exécution sur le premier thread disponible.

Si l'opération d'E / S est exécutée de manière synchrone, elle maintient son thread en cours d'exécution sans rien faire tant que l'opération n'est pas terminée. Le runtime ne sait pas quand l'opération d'E / S se termine, donc il fournira périodiquement du temps CPU au thread en attente, temps CPU qui aurait pu autrement être utilisé par d'autres threads qui ont 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 plutôt une meilleure évolutivité. Cela est évident lors de l'exécution dans des contextes qui ont un nombre limité de threads disponibles, 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 requête. 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 de l'application Web ou de ralentissement de la réponse.

Une chose que j'ai remarquée est que les E / S asynchrones ne sont pas la meilleure option lorsqu'il s'agit d'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 soit lancée sur un thread et terminée sur un autre ajoute une surcharge à l'exécution globale.

Vous pouvez lire une recherche plus détaillée que j'ai récemment effectuée sur le sujet des E / S asynchrones et du multithreading ici .

Florin Dumitrescu
la source
Je me demande s'il vaudrait la peine de faire une distinction entre les opérations d'E / S qui devraient se terminer et les choses qui ne pourraient pas [par exemple, "obtenir le prochain caractère qui arrive sur un port série", dans les cas où le périphérique distant peut ou non envoyer quoi que ce soit]. Si une opération d'E / S est censée se terminer dans un délai raisonnable, on peut retarder le nettoyage des ressources associées jusqu'à la fin de l'opération. Cependant, si l’opération n’était jamais terminée, un tel retard serait déraisonnable.
supercat
@supercat le scénario que vous décrivez est utilisé dans les applications et bibliothèques de niveau inférieur. Les serveurs en dépendent, car ils attendent continuellement les connexions entrantes. Les E / S asynchrones décrites ci-dessus ne peuvent pas s'intégrer dans ce scénario car elles sont basées sur le démarrage d'une opération spécifique et l'enregistrement d'un rappel pour son achèvement. Dans le cas que vous décrivez, vous devez enregistrer un rappel sur un événement système et traiter chaque notification. Vous traitez continuellement les entrées plutôt que d'effectuer des opérations. Comme dit, cela se fait généralement à bas niveau, presque jamais dans vos applications.
Florin Dumitrescu
Le modèle est assez courant avec les applications fournies avec différents types de matériel. Les ports série ne sont plus aussi courants qu'avant, mais les puces USB qui émulent les ports série sont assez populaires dans la conception de matériel spécialisé. Les caractères de telles choses sont traités au niveau de l'application, puisque le système d'exploitation n'aura aucun moyen de savoir qu'une séquence de caractères d'entrée signifie par exemple qu'un tiroir-caisse a été ouvert et qu'une notification doit être envoyée quelque part.
supercat
Je ne pense pas que la partie sur le coût CPU du blocage des E / S soit précise: en état de blocage, un thread qui a déclenché le blocage des E / S est mis en attente par le système d'exploitation et ne coûte pas de périodes CPU jusqu'à ce que l'E / S soit complètement terminée, seulement après quoi le système d'exploitation (notifie-t-il par des interruptions) reprend-il le thread bloqué. Ce que vous avez décrit (attente occupée par une longue interrogation) n'est pas la façon dont le blocage des E / S est implémenté dans presque tous les runtime / compilateurs.
Lifu Huang
4

La principale raison d'utiliser AIO est l'évolutivité. Considérés dans le contexte de quelques fils, les avantages ne sont pas évidents. Mais lorsque le système passe à 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.

fissurezone
la source
4

Pour présumer une amélioration de la vitesse due à toute forme de multi-calcul, vous devez présumer soit que plusieurs tâches basées sur le processeur sont exécutées simultanément sur plusieurs ressources informatiques (généralement des cœurs de processeur), soit 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 système (stockage sur disque, par exemple) tandis que certaines tâches dépendent d'un autre (réception de la communication d'un périphérique) et d'autres encore 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 «simultanée» ou «asynchrone», bien que «simultanée» soit parfois également utilisée pour désigner le cas de la simple autorisation d'un système d'exploitation à entrelacer l'exécution de plusieurs tâches, que cette exécution doive ou non prendre place en série ou si plusieurs ressources peuvent être utilisées pour réaliser une exécution parallèle. Dans ce dernier cas, «concurrent» 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 sont prompts à faire une réclamation telle que «Les E / S asynchrones seront plus rapides que les E / S multi-thread». Cette affirmation est douteuse pour plusieurs raisons. Premièrement, il se peut que certains frameworks d'E / S asynchrones soient implémentés précisément avec le multi-threading, auquel cas ils sont un dans le même et cela n'a pas de sens de dire qu'un concept "est plus rapide que" l'autre .

Deuxièmement, même dans le cas où il existe une implémentation à un seul thread d'un framework asynchrone (comme une boucle d'événements à un seul thread), vous devez toujours faire une hypothèse sur ce que fait cette boucle. Par exemple, une chose stupide que vous pouvez faire avec une boucle d'événement à thread unique est de lui demander d'effectuer de manière asynchrone deux tâches différentes purement liées au processeur. Si vous avez fait cela sur une machine avec seulement un cœur de processeur unique idéalisé (en ignorant les optimisations matérielles modernes), l'exécution de cette tâche "asynchrone" ne serait pas vraiment différente de celle avec deux threads gérés indépendamment, ou avec un seul processus - - la différence peut résulter du changement de contexte de thread ou des optimisations de planification du système d'exploitation, mais si les deux tâches vont au CPU, ce serait similaire 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 au processeur sur une machine avec exactement un cœur de processeur.

L'exécution multithread n'a pas besoin d'être simultanée: vous créez deux threads sur une machine avec un seul cœur de processeur, ou demandez à deux threads d'acquérir tout autre type de ressource rare (imaginez, par exemple, une base de données réseau qui ne peut en établir qu'une connexion à la fois). L'exécution des threads peut être entrelacée mais le planificateur du système d'exploitation le juge opportun, mais leur durée d'exécution totale ne peut pas être réduite (et sera augmentée à partir du changement de contexte de thread) sur un seul cœur (ou plus généralement, si vous créez plus de threads qu'il n'y en a cœurs pour les exécuter, ou avoir plus de threads demandant une ressource que ce que la ressource peut supporter). Il en va de même pour le multi-traitement.

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

Cependant, si vous définissez un cas d'utilisation spécifique, comme un programme spécifique qui effectue à la fois un appel réseau pour récupérer des données à partir d'une ressource connectée au réseau comme une base de données distante et effectue également des calculs locaux liés au processeur, vous pouvez commencer à raisonner les différences de performances entre les deux méthodes étant donné une hypothèse particulière sur le matériel.

Les questions à se poser: Combien d'étapes de calcul dois-je effectuer 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 qui nécessitent l'utilisation de sous-composants système indépendants et qui peuvent en bénéficier 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 largement sur des sous-systèmes indépendants, une solution asynchrone peut être bonne. Si le nombre de threads nécessaires pour le gérer était important, de sorte que le changement de contexte devenait non trivial pour le système d'exploitation, alors une solution asynchrone à thread unique pourrait être meilleure.

Chaque fois que 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 une surcharge insatisfaisante, et tandis que l'asynchronie monothread peut introduire moins de surcharge, dans une telle ressource- situation limitée, il ne peut pas non plus produire une accélération. Dans un tel cas, la seule option (si vous voulez une accélération) est de rendre plusieurs copies de cette ressource disponibles (par exemple, plusieurs cœurs de processeur si la ressource rare est le processeur; une meilleure base de données qui prend en charge plus de connexions simultanées si la ressource rare est une base de données à connexion limitée, etc.).

Une autre façon de le dire est: permettre au système d'exploitation d'entrelacer l'utilisation d'une seule ressource 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 de laisser la deuxième tâche se terminer en série. En outre, le coût de l'entrelacement par l'ordonnanceur signifie que dans toute situation réelle, il crée en fait un ralentissement. Peu importe si l'utilisation entrelacée se produit du processeur, d'une ressource réseau, d'une ressource mémoire, d'un périphérique ou de toute autre ressource système.

ely
la source
2

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

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

Miguel
la source
2

Je suis actuellement en train d'implémenter async io sur une plateforme embarquée utilisant des protothreads. Non bloquant io fait la différence entre fonctionner à 16000fps et 160fps. Le plus grand avantage de non bloquant io est que vous pouvez structurer votre code pour faire d'autres choses pendant que le matériel fait son travail. Même l'initialisation des appareils peut être effectuée en parallèle.

Martin

user2826084
la source
1

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

"Donc, oui, NodeJS est monothread, mais c'est une demi-vérité, en fait il est piloté par les événements et monothread avec des nœuds de calcul en arrière-plan. La boucle d'événements principale est monothread mais la plupart des travaux d'E / S s'exécutent sur des threads séparés, car les API d'E / S dans Node.js sont asynchrones / non bloquantes par conception, afin de s'adapter à la boucle d'événements. "

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

"Node.js n'est pas bloquant, ce qui signifie que toutes les fonctions (rappels) 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 "Node est plus rapide car non bloquant ..." est un peu marketing et c'est une excellente question. C'est efficace et évolutif, mais pas exactement avec un seul thread.

Cheminée
la source
0

L'amélioration pour autant que je sais est que les utilisations entrées / sorties asynchrones (je parle de MS système, juste pour préciser) que l'on appelle E / S ports d'achèvement . En utilisant l'appel asynchrone, le framework 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 sentirez sensiblement votre application plus réactive si vous préférez AsyncCalls au lieu de bloquer les threads.

Felice Pollano
la source
0

Permettez-moi de vous donner un contre-exemple selon lequel les E / S asynchrones ne fonctionnent pas. J'écris un proxy similaire à ci-dessous en utilisant boost :: asio. https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp

Cependant, le scénario de mon cas est que les messages entrants (du côté des clients) sont rapides tandis que les messages sortants (du côté du serveur) sont lents pendant une session, pour suivre la vitesse entrante ou pour maximiser le débit total du proxy, nous devons utiliser plusieurs sessions sous une seule connexion.

Ainsi, ce cadre d'E / S asynchrone ne fonctionne plus. Nous avons besoin d'un pool de threads à envoyer au serveur en attribuant à chaque thread une session.

Zhidian Du
la source