Quelle est la différence entre epoll, poll, threadpool?

Quelqu’un pourrait-il expliquer quelle est la différence entre epoll , poll et threadpool?

  • Quels sont les avantages / inconvénients?
  • Des suggestions pour les frameworks?
  • Des suggestions de tutoriels simples / basiques?
  • Il semble que epoll et poll soient spécifiques à Linux … Existe-t-il une alternative équivalente pour Windows?

    Threadpool ne correspond pas vraiment à la même catégorie que poll et epoll, donc je suppose que vous faites référence à threadpool comme dans “threadpool pour gérer de nombreuses connexions avec un thread par connexion”.

    Avantages et inconvénients

    • threadpool
      • Raisonnablement efficace pour les petites et moyennes concurrences, peut même surpasser les autres techniques.
      • Utilise plusieurs cœurs.
      • Ne va pas bien au-delà de “plusieurs centaines”, même si certains systèmes (par exemple Linux) peuvent en principe planifier des centaines de milliers de threads.
      • La mise en œuvre naïve présente un problème de ” troupeau tonitruant “.
      • Mis à part le changement de contexte et l’augmentation des effectifs, il faut considérer la mémoire. Chaque thread a une stack (généralement au moins un mégaoctet). Un millier de threads prennent donc un gigaoctet de RAM juste pour la stack. Même si cette mémoire n’est pas validée, elle élimine encore beaucoup d’espace d’adressage sous un système d’exploitation 32 bits (pas vraiment un problème sous 64 bits).
      • Les threads peuvent en fait utiliser epoll , bien que la méthode évidente (tous les threads bloquent sur epoll_wait ) ne soit d’aucune utilité, car epoll réveillera tous les threads qui l’attendent, donc il aura toujours les mêmes problèmes.
        • Solution optimale: un seul thread écoute epoll, effectue le multiplexage en entrée et transmet les requêtes complètes à un pool de threads.
        • futex est votre ami ici, en combinaison avec par exemple une file d’attente rapide par thread. Bien que mal documenté et peu maniable, le futex offre exactement ce futex vous futex besoin. epoll peut renvoyer plusieurs événements à la fois, et futex vous permet efficacement et de manière précise de contrôler N threads bloqués à la fois (N étant idéalement min(num_cpu, num_events) ), et dans le meilleur des cas, il ne nécessite pas de syscall / changement de contexte du tout.
        • Pas sortingvial à mettre en œuvre, prend un peu de soin.
    • fork (aka old fashion threadpool)
      • Raisonnablement efficace pour les petites et moyennes concurrences.
      • Ne pas évoluer bien au-delà de “quelques centaines”.
      • Les changements de contexte sont beaucoup plus coûteux (espaces d’adressage différents!).
      • Pèse beaucoup moins sur les anciens systèmes où la fourchette est beaucoup plus chère (copie profonde de toutes les pages). Même sur les systèmes modernes, fork n’est pas “gratuit”, bien que la surcharge soit principalement coalisée par le mécanisme de copie sur écriture. Sur les jeux de données volumineux qui sont également modifiés , un nombre considérable de défauts de page après la fork peut avoir un impact négatif sur les performances.
      • Cependant, il a été prouvé qu’il fonctionne de manière fiable depuis plus de 30 ans.
      • Ridiculement facile à mettre en œuvre et solide: si l’un des processus tombe en panne, le monde ne s’arrête pas. Il n’y a (presque) rien que vous puissiez faire de mal.
      • Très enclin à “troupeau tonitruant”.
    • poll / select
      • Deux saveurs (BSD vs System V) de plus ou moins la même chose.
      • Une utilisation plutôt ancienne et lente, quelque peu délicate, mais il n’ya pratiquement aucune plate-forme qui ne les supporte pas.
      • Attend que “quelque chose arrive” sur un ensemble de descripteurs
        • Permet à un thread / processus de gérer plusieurs requêtes à la fois.
        • Aucune utilisation multi-core.
      • Vous devez copier la liste des descripteurs de l’utilisateur à l’espace kernel chaque fois que vous attendez. Doit effectuer une recherche linéaire sur les descripteurs. Cela limite son efficacité.
      • Ne s’adapte pas bien aux “milliers” (en fait, limite ssortingcte autour de 1024 sur la plupart des systèmes, ou même 64 sur certains).
      • Utilisez-le car il est portable si vous ne gérez de toute façon qu’une dizaine de descripteurs (pas de problèmes de performances), ou si vous devez supporter des plates-formes qui n’ont rien de mieux. Ne pas utiliser autrement.
      • D’un sharepoint vue conceptuel, un serveur devient un peu plus compliqué qu’un serveur fourchu, car vous devez maintenant gérer de nombreuses connexions et une machine d’état pour chaque connexion. Vous devez multiplexer les requêtes dès leur arrivée, assembler des requêtes partielles, etc. le serveur connaît simplement un socket unique (enfin deux, en comptant le socket), lit jusqu’à ce qu’il ait ce qu’il veut ou jusqu’à ce que la connexion soit à moitié fermée, puis écrit ce qu’il veut. Il ne se préoccupe pas du blocage, de la préparation ou de la famine, pas plus que de certaines données non liées qui arrivent, c’est le problème d’un autre processus.
    • epoll
      • Linux uniquement
      • Concept de modifications coûteuses vs attentes efficaces:
        • Copie les informations sur les descripteurs dans l’espace kernel lorsque des descripteurs sont ajoutés ( epoll_ctl )
          • C’est généralement quelque chose qui arrive rarement .
        • Il n’est pas nécessaire de copier des données dans l’espace kernel lorsque vous attendez des événements ( epoll_wait )
          • C’est généralement quelque chose qui arrive très souvent .
        • Ajoute le serveur (ou plutôt sa structure epoll) aux files d’attente des descripteurs
          • Le descripteur sait donc qui écoute et signale directement les serveurs, le cas échéant, plutôt que les serveurs recherchant une liste de descripteurs
          • Manière opposée au fonctionnement du poll
          • O (1) avec k petit (très rapide) par rapport au nombre de descripteurs, au lieu de O (n)
      • Fonctionne très bien avec timerfd et eventfd (une résolution et une précision étonnantes de la timer).
      • Fonctionne bien avec signalfd , éliminant la manipulation délicate des signaux, en les faisant partie du stream de contrôle normal de manière très élégante.
      • Une instance epoll peut héberger d’autres instances epoll de manière récursive
      • Hypothèses formulées par ce modèle de programmation:
        • La plupart des descripteurs sont la plupart du temps inactifs, peu de choses (par exemple “données reçues”, “connexion fermée”) se produisent sur quelques descripteurs.
        • La plupart du temps, vous ne souhaitez pas append / supprimer de descripteurs de l’ensemble.
        • La plupart du temps, vous attendez quelque chose.
      • Quelques pièges mineurs:
        • Un epoll déclenché par un niveau éveille tous les threads qui l’attendent (ceci fonctionne “comme prévu”), donc la manière naïve d’utiliser epoll avec un pool de threads est inutile. Au moins pour un serveur TCP, ce n’est pas un gros problème car les requêtes partielles devront d’abord être assemblées, de sorte qu’une implémentation naïve multithread ne fonctionnera pas dans les deux cas.
        • Ne fonctionne pas comme on pourrait s’y attendre avec le fichier read / write (“toujours prêt”).
        • Ne pouvait pas être utilisé avec AIO jusqu’à récemment, maintenant possible via eventfd , mais nécessite une fonction non documentée (à ce jour).
        • Si les hypothèses ci-dessus ne sont pas vraies, epoll peut être inefficace et le poll peut être aussi performant que possible.
        • epoll ne peut pas faire de la «magie», c’est-à-dire qu’il est toujours nécessaire que le nombre d’ événements se produise .
        • Cependant, epoll joue bien avec le nouveau recvmmsg recvmmsg, car il renvoie plusieurs notifications de préparation à la fois (autant qu’il est disponible, jusqu’à ce que vous spécifiez comme maxevents ). Cela permet de recevoir par exemple 15 notifications EPOLLIN avec un appel système sur un serveur occupé et de lire les 15 messages correspondants avec un deuxième appel système (une réduction de 93% des appels système!). Malheureusement, toutes les opérations sur une recvmmsg recvmmsg se rapportent à la même socket, donc c’est surtout utile pour les services basés sur UDP (pour TCP, il devrait y avoir une sorte de recvmmsmsg recvmmsmsg qui prend également un descripteur de socket!).
        • Les descripteurs doivent toujours être définis sur non-bloquants et EAGAIN doit être vérifié même en utilisant epoll car il existe des situations exceptionnelles où l’état des rapports epoll et une lecture (ou écriture) ultérieure sont toujours bloqués. C’est également le cas pour poll / select sur certains kernelx (même si cela a probablement été corrigé).
        • Avec une implémentation naïve , la famine des expéditeurs lents est possible. Lorsque vous lisez en aveugle jusqu’à ce que EAGAIN soit renvoyé lors de la réception d’une notification, il est possible de lire indéfiniment les nouvelles données entrantes d’un expéditeur rapide tout en affamant complètement un expéditeur lent (tant que les données arrivent assez rapidement, tandis que!). S’applique à poll / select de la même manière.
        • Le mode déclenché par des arêtes présente des particularités et un comportement inattendu dans certaines situations, car la documentation (pages de manuel et TLPI) est vague (“probablement”, “devrait”, “pourrait”) et parfois trompeuse quant à son fonctionnement.
          La documentation indique que plusieurs threads en attente sur un epoll sont tous signalés. Il indique en outre qu’une notification vous indique si l’activité d’E / S s’est produite depuis le dernier appel à epoll_wait (ou depuis l’ouverture du descripteur, s’il n’y a pas eu d’appel précédent).
          Le vrai comportement observable en mode déclenché par front est beaucoup plus proche de “réveille le premier thread qui a appelé epoll_wait , signalant que l’activité des epoll_wait -sorties s’est produite depuis quiconque a appelé epoll_wait ou une fonction lecture / écriture sur le descripteur, et préparation à la prochaine thread appelant ou déjà bloqué dans epoll_wait , pour toutes les opérations se produisant après que quelqu’un ait appelé une fonction de lecture (ou d’écriture) sur le descripteur “. Ça a du sens aussi… ce n’est pas exactement ce que la documentation suggère.
    • kqueue
      • Analogon BSD à epoll , utilisation différente, effet similaire.
      • Fonctionne également sur Mac OS X
      • Rumour d’être plus rapide (je ne l’ai jamais utilisé, donc je ne peux pas dire si c’est vrai).
      • Enregistre les événements et renvoie un jeu de résultats dans un seul appel système.
    • Ports d’achèvement IO
      • Epoll for Windows, ou plutôt epoll sur les stéroïdes.
      • Fonctionne de manière transparente avec tout ce qui est accessible ou pouvant être alerté (sockets, minuteurs, opérations de fichiers, threads, processus)
      • Si Microsoft avait une chose dans Windows, ce sont les ports d’achèvement:
        • Fonctionne sans souci avec un nombre illimité de threads
        • Pas de troupeau tonitruant
        • Réveille les fils un par un dans un ordre LIFO
        • Garde les caches au chaud et minimise les changements de contexte
        • Respecte le nombre de processeurs sur la machine ou délivre le nombre de travailleurs souhaité
      • Permet à l’application de publier des événements, ce qui se prête à une implémentation de file de travail parallèle très simple, fiable et efficace (planifie plus de 500 000 tâches par seconde sur mon système).
      • Désavantage mineur: ne supprime pas facilement les descripteurs de fichiers une fois ajoutés (doit être fermé et rouvert).

    Cadres

    libevent – La version 2.0 prend également en charge les ports d’achèvement sous Windows.

    ASIO – Si vous utilisez Boost dans votre projet, ne cherchez plus: vous disposez déjà de cette option en tant que boost-asio.

    Des suggestions de tutoriels simples / basiques?

    Les frameworks listés ci-dessus sont fournis avec une documentation complète. Les documents Linux et MSDN expliquent en détail les ports epoll et d’achèvement.

    Mini-tutoriel pour utiliser epoll:

     int my_epoll = epoll_create(0); // argument is ignored nowadays epoll_event e; e.fd = some_socket_fd; // this can in fact be anything you like epoll_ctl(my_epoll, EPOLL_CTL_ADD, some_socket_fd, &e); ... epoll_event evt[10]; // or whatever number for(...) if((num = epoll_wait(my_epoll, evt, 10, -1)) > 0) do_something(); 

    Mini-tutoriel pour les ports d’achèvement d’E / S (notez l’appel de CreateIoCompletionPort deux fois avec des parameters différents):

     HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0); // equals epoll_create CreateIoCompletionPort(mySocketHandle, iocp, 0, 0); // equals epoll_ctl(EPOLL_CTL_ADD) OVERLAPPED o; for(...) if(GetQueuedCompletionStatus(iocp, &number_bytes, &key, &o, INFINITE)) // equals epoll_wait() do_something(); 

    (Ces mini-tuts omettent toutes sortes de vérifications d’erreurs, et j’espère que je n’ai pas fait de fautes de frappe, mais elles devraient pour la plupart être correctes pour vous donner une idée.)

    MODIFIER:
    Notez que les ports d’achèvement (Windows) fonctionnent de manière conceptuelle à l’inverse d’epoll (ou kqueue). Ils signalent, comme leur nom l’indique, l’ achèvement et non la préparation . C’est-à-dire que vous lancez une requête asynchrone et que vous l’oubliez jusqu’à ce que quelque temps plus tard vous sachiez qu’elle s’est terminée (avec succès ou pas, et il y a le cas exceptionnel de “terminé immédiatement”).
    Avec epoll, vous bloquez jusqu’à ce que vous soyez averti que “certaines données” (éventuellement aussi peu qu’un octet) sont arrivées et sont disponibles ou qu’il y a suffisamment d’espace tampon pour que vous puissiez effectuer une opération d’écriture sans blocage. Ce n’est qu’à ce moment-là que vous lancez l’opération réelle, qui ne bloquera probablement pas (autre que ce à quoi vous vous attendez, il n’ya pas de garantie ssortingcte), il est donc conseillé de définir des descripteurs non bloquants et de vérifier EAGAIN [EAGAIN pour les sockets, parce que oh joie, la norme permet deux valeurs d’erreur différentes]).