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 détails techniques sur le blocage des E / S et des E / S non bloquantes et j’ai constaté que plusieurs personnes estimaient que les E / S non bloquantes seraient plus rapides que le blocage des E / S. Par exemple dans ce document .

Si j’utilise des E / S bloquantes, alors le thread actuellement bloqué ne peut rien faire d’autre, car 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 d’un processeur et qui n’est pas bloqué, il ne devrait pas y avoir plus de temps d’inactivité du processeur par rapport à une approche non bloquante basée sur des événements?

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 le temps système induit par le changement de threads. 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 le voir fonctionner:

  1. Pour charger le contenu d’un fichier, une application délègue cette tâche à un environnement d’E / S basé sur des événements, en transmettant une fonction de rappel avec un nom de fichier.
  2. Le cadre d’événement délègue au système d’exploitation, qui programme un contrôleur DMA du disque dur pour écrire le fichier directement en mémoire
  3. Le framework d’événements permet l’exécution de code supplémentaire.
  4. Une fois la copie de disque à mémoire terminée, le contrôleur DMA provoque une interruption.
  5. Le gestionnaire d’interruption du système d’exploitation notifie le cadre d’E / S basé sur les événements à propos du chargement complet du fichier en mémoire. Comment ça fait ça? En utilisant un signal ??
  6. Le code qui est actuellement exécuté dans l’événement i / o Framework se termine.
  7. Le framework d’E / S basé sur les événements vérifie sa queue et voit le message du système d’exploitation de l’étape 5 et exécute le rappel obtenu à l’étape 1.

Est-ce comme ça que ça marche? 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 stack (par exemple, un ordonnanceur réel qui aurait besoin de sauvegarder la stack et de copier la stack d’un autre thread en mémoire lors de la commutation des threads)? Combien de temps cela économise-t-il réellement? Y a-t-il plus à cela?

    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 y parvenir en utilisant un thread supplémentaire. Comme vous l’avez dit pour la meilleure performance globale (système), je pense qu’il serait préférable d’utiliser des E / S asynchrones et non des threads multiples (réduisant ainsi la commutation des threads).

    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 bloquer les E / S, mais peut également être une E / S non bloquante).
      Chaque thread nécessite des ressources mémoire (également la mémoire du kernel!), Ce qui est un inconvénient. Et chaque thread supplémentaire signifie plus de travail pour le planificateur.
    2. Un thread pour toutes les connexions.
      Cela prend la charge du système parce que nous avons moins de threads. Mais cela vous empêche également d’utiliser toutes les performances de votre machine, car vous pourriez finir par utiliser 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 en soi un problème. Comme vous avez pu le constater, j’ai choisi un nombre élevé de connexions / threads. Je doute que vous verriez 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 sur le blog MSDN. Windows a-t-il une limite de 2000 threads par processus? ).

    Sous Windows utilisant un fichier sans tampon, les E / S signifient que les écritures doivent avoir une taille qui est un multiple de la taille de la page. Je ne l’ai pas testé, mais il semblerait que cela puisse également affecter positivement les performances en écriture pour les écritures synchrones et asynchrones mises en mémoire 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 structure OVERLAPPED ) en utilisant un événement ou un rappel. Les fonctions de rappel ne seront appelées que par exemple lorsque votre code appelle WaitForMultipleObjectsEx avec bAlertable défini sur true .

    Un peu plus de lecture sur le web:

    • Plusieurs threads dans l’interface utilisateur sur MSDN, traitant également rapidement le coût de création de threads
    • Section Threads et pools de threads : “Bien que les threads soient relativement faciles à créer et à utiliser, le système d’exploitation consacre beaucoup de temps et d’autres ressources à les gérer.”
    • La documentation CreateThread sur MSDN indique “Cependant, votre application aura de meilleures performances si vous créez un thread par processeur et générez des files d’attente de demandes pour lesquelles l’application conserve les informations de contexte.”.
    • Ancien article Pourquoi trop de threads nuisent aux performances et que faire à ce sujet

    Les E / S incluent plusieurs types d’opérations comme la lecture et l’écriture de données à partir de disques durs, l’access à des ressources réseau, l’appel de services Web ou l’extraction de données à partir de bases de données. Selon la plate-forme et le type d’opération, les E / S asynchrones tireront généralement parti de toute prise en charge de matériel ou de 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 empêchent les 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, 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 queue 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 ne fait rien jusqu’à la fin de l’opération. Le moteur d’exécution ne sait pas quand l’opération d’E / S s’achève, il fournira donc périodiquement du temps processeur au thread en attente, le temps CPU qui aurait pu être utilisé par d’autres threads ayant des opérations liées au processeur.

    Ainsi, comme @ user1629468 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 ayant un nombre limité de threads disponibles, comme c’est le cas avec les applications Web. L’application Web utilise généralement un pool de threads à partir duquel elle atsortingbue des threads à chaque requête. Si les requêtes sont bloquées pendant de longues opérations d’E / S, il existe un risque d’épuisement du pool Web et de ralentissement 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 occuper un thread en attendant que l’opération d’E / S se termine n’est pas très important et le fait que l’opération soit démarrée sur un thread et qu’il soit terminé sur un autre ajoute une surcharge à l’exécution globale.

    Vous pouvez lire ici une recherche plus détaillée sur le sujet des E / S asynchrones par rapport au multithreading.

    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 évolue à des milliers de threads, AIO offrira de bien meilleures performances. La mise en garde est que la bibliothèque AIO ne devrait pas introduire d’autres goulots d’étranglement.

    Pour supposer une amélioration de la vitesse due à toute forme de multi-informatique, vous devez présumer 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) ou que toutes les tâches ne dépendent pas de 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) alors que certaines tâches en dépendent (réception de la communication d’un périphérique) et 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 “concurrent” soit parfois utilisé pour désigner simplement le fait de permettre à un système d’exploitation d’entrelacer l’exécution de plusieurs tâches, que cette exécution doive placer en série ou si plusieurs ressources peuvent être utilisées pour réaliser une exécution parallèle. Dans ce dernier cas, “concurrent” se réfère généralement à la façon dont l’exécution est écrite dans le programme, plutôt que sous l’angle 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 multithread». Cette affirmation est douteuse pour plusieurs raisons. Tout d’abord, il se peut que certains frameworks d’E / S asynchrones soient implémentés précisément avec le multi-threading, auquel cas ils ne font qu’un et il n’est pas logique de dire qu’un concept “est plus rapide que l’autre”. .

    Deuxièmement, même dans le cas d’une implémentation mono-thread d’un framework asynchrone (comme une boucle d’événement mono-thread), vous devez quand même 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 à thread unique est de demander qu’elle effectue de manière asynchrone deux tâches purement liées au processeur. Si vous avez fait cela sur une machine avec un cœur de processeur unique idéalisé (sans tenir compte des optimisations matérielles modernes), alors cette tâche “asynchrone” ne fonctionnerait pas vraiment différemment avec deux threads gérés indépendamment, ou avec un seul processus – – la différence pourrait être le changement de contexte ou l’optimisation du planning du système d’exploitation, mais si les deux tâches sont dirigées vers le processeur, elles seraient similaires dans les deux cas.

    Il est utile d’imaginer un grand nombre des cas de coin 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 multi-thread ne doit pas nécessairement être simultanée: vous générez deux threads sur une machine avec un cœur de processeur unique, ou demandez à deux threads d’acquérir un autre type de ressource rare (imaginez, par exemple, une firebase database réseau qui ne peut en établir qu’un) connexion à la fois). L’exécution des threads peut être nestede, quel que soit le programme d’exécution du système d’exploitation, mais leur exécution totale ne peut pas être réduite (et augmentée à partir du changement de contexte de thread) sur un seul core (ou plus généralement si des cœurs pour les exécuter ou avoir plus de threads demandant une ressource que ce que la ressource peut supporter). Cette même chose vaut également pour le multi-traitement.

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

    Si vous définissez un cas d’utilisation spécifique, cependant, comme 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 firebase database distante et effectue également des calculs locaux liés au processeur, vous pouvez commencer à raisonner les différences de performance entre les deux méthodes en fonction d’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 sont disponibles pour les exécuter? Existe-t-il des sous-ensembles d’étapes de calcul qui nécessitent l’utilisation de sous-composants de système indépendants et peuvent en bénéficier simultanément? Combien de cœurs de processeur ai-je et quel est le coût d’utilisation de plusieurs processeurs ou threads pour effectuer des tâches sur des cœurs distincts?

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

    Chaque fois que les tâches sont liées à la même ressource (par exemple, plusieurs access simultanés au même réseau ou à la même ressource locale), le multithread introduira probablement une surcharge insatisfaisante, tandis que l’asynchronisme mono-thread risque d’ introduire moins de ressources. situation limitée elle aussi ne peut pas produire une accélération. Dans ce cas, la seule option (si vous souhaitez accélérer) est de rendre disponibles plusieurs copies de cette ressource (par exemple, plusieurs cœurs de processeur si la ressource rare est le processeur; une meilleure firebase database prenant en charge davantage de connexions simultanées). est une firebase database limitée, etc.).

    Une autre façon de le dire est de 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 laisser une tâche utiliser la ressource pendant que l’autre attend, puis laisser la deuxième tâche se terminer en série. En outre, le coût du planificateur de l’entrelacement signifie que dans toute situation réelle, il crée un ralentissement. Peu importe si l’utilisation entrelacée se produit sur le processeur, une ressource réseau, une ressource mémoire, un périphérique ou toute autre ressource système.

    Une des implémentations possibles des 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 des E / S via un mécanisme de rappel. En fait, c’est comme ça que fonctionne le module AIO dans la glibc. Voici quelques détails vagues sur la mise en œuvre.

    Bien que cette solution soit relativement portable (à condition que vous ayez des threads), le système d’exploitation est généralement capable de traiter plus efficacement les E / S non bloquantes. Cet article Wikipedia répertorie les implémentations possibles en plus du pool de threads.

    Je suis actuellement en train d’implémenter async io sur une plateforme embarquée utilisant des protothreads. Le blocage de io fait la différence entre un fonctionnement à 16000fps et 160fps. Le plus grand avantage de ne pas bloquer 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 périphériques peut être effectuée en parallèle.

    Martin

    L’amélioration, à ma connaissance, est que les E / S asynchrones utilisent (je parle de MS System, juste pour clarifier) ​​les ports d’achèvement d’E / S. 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 sentiriez sensiblement votre application plus réactive si vous préférez AsyncCalls au lieu de bloquer les threads.