Threadpool ne rentre pas vraiment dans 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 simultanées, peut même surpasser les autres techniques.
- Utilise plusieurs cœurs.
- N'évolue pas bien au-delà de "plusieurs centaines" même si certains systèmes (par exemple Linux) peuvent en principe programmer correctement 100 000 threads.
- L'implémentation naïve présente un problème " troupeau tonitruant ".
- Outre le changement de contexte et le troupeau tonitruant, il faut tenir compte de la mémoire. Chaque thread a une pile (généralement au moins un mégaoctet). Un millier de threads prennent donc un gigaoctet de RAM juste pour la pile. Même si cette mémoire n'est pas validée, elle enlève toujours un espace d'adressage considérable sous un système d'exploitation 32 bits (pas vraiment un problème sous 64 bits).
- Threadscanutilise réellement
epoll
, bien que la manière évidente (tous les threads se bloquent sur _epoll_wait
_) ne sert à rien, car epoll se réveillerachaque threaden attente, il aura donc toujours les mêmes problèmes. - Solution optimale: un seul thread écoute sur epoll, effectue le multiplexage d'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 d'avance rapide par thread. Bien que mal documenté et peu maniable, futex
offre exactement ce dont vous avez besoin. epoll
peut renvoyer plusieurs événements à la fois, et futex
vous permet de vous réveiller de manière efficace et précise N threads bloqués à la fois (N étant min(num_cpu, num_events)
idéalement), et dans le meilleur des cas, cela n'implique pas du tout un commutateur syscall/context supplémentaire.- Pas banale à mettre en œuvre, prend un peu de soin.
fork
(alias threadpool à l'ancienne) - Raisonnablement efficace pour les petites et moyennes simultanées.
- N'échelle pas bien au-delà de "quelques centaines".
- Les commutateurs de contexte sontbeaucoupplus chers (différents espaces d'adressage!).
- Évolue considérablement moins bien sur les anciens systèmes où fork est beaucoup plus cher (copie complète de toutes les pages). Même sur les systèmes modernes,
fork
n'est pas "gratuit", bien que la surcharge soit principalement fusionnée par le mécanisme de copie sur écriture. Sur les grands ensembles de données qui sontégalement modifiés, un nombre considérable de défauts de page suivant fork
peut avoir un impact négatif sur les performances. - Cependant, il a fait ses preuves pour fonctionner 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 au "troupeau tonitruant".
poll
/select
- Deux saveurs (BSD vs System V) de plus ou moins la même chose.
- Utilisation quelque peu ancienne et lente, quelque peu délicate, mais il n'y a pratiquement aucune plate-forme qui ne les prend pas en charge.
- Attend que "quelque chose se passe" sur un ensemble de descripteurs
- Permet à un thread/processus de gérer de nombreuses demandes à la fois.
- Pas d'utilisation multicœur.
- Doit copier la liste des descripteurs de l'utilisateur vers l'espace noyau à chaque fois que vous attendez. Doit effectuer une recherche linéaire sur les descripteurs. Cela limite son efficacité.
- N'évolue pas bien à des "milliers" (en fait, limite stricte autour de 1024 sur la plupart des systèmes, ou aussi bas que 64 sur certains).
- Utilisez-le car il est portable si vous ne traitez de toute façon qu'avec une douzaine de descripteurs (aucun problème de performances) ou si vous devez prendre en charge des plateformes qui n'ont rien de mieux. N'utilisez pas autrement.
- Conceptuellement, un serveur devient un peu plus compliqué qu'un forké, car vous devez maintenant maintenir de nombreuses connexions et une machine d'état pour chaque connexion, et vous devez multiplexer entre les requêtes au fur et à mesure qu'elles arrivent, assembler des requêtes partielles, etc. le serveur ne connaît qu'une seule prise (enfin, deux, en comptant la prise d'écoute), 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 soucie pas du blocage ou de la préparation ou de la famine, ni de l'arrivée de données non liées, c'est un autre problème du processus.
epoll
- Linux uniquement.
- Concept de modifications coûteuses par rapport à des attentes efficaces:
- Copie les informations sur les descripteurs dans l'espace du noyau lorsque des descripteurs sont ajoutés (_
epoll_ctl
_) - C'est généralement quelque chose qui se produitrarement.
- Est-ce quepasdoit copier des données dans l'espace du noyau en attendant des événements (_
epoll_wait
_) - C'est généralement quelque chose qui se produittrè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 qui recherchent une liste de descripteurs
- Manière opposée du fonctionnement de
poll
- O(1) with small k (very fast) in respect of the number of descriptors, instead of O(n)
- Fonctionne très bien avec
timerfd
et eventfd
(résolution et précision de la minuterie étonnantes aussi). - Fonctionne bien avec
signalfd
, éliminant la manipulation maladroite des signaux, les faisant partie du flux de contrôle normal d'une manière très élégante. - Une instance epoll peut héberger d'autres instances epoll récursivement
- Hypothèses formulées par ce modèle de programmation:
- La plupart des descripteurs sont inactifs la plupart du temps, peu de choses (par exemple, "données reçues", "connexion fermée") se produisent réellement sur quelques descripteurs.
- La plupart du temps, vous ne voulez pas ajouter/supprimer de descripteurs de l'ensemble.
- La plupart du temps, vous attendez que quelque chose se passe.
- Quelques pièges mineurs:
- Un epoll déclenché par niveau réveille tous les threads qui l'attendent (cela "fonctionne comme prévu"), donc la façon 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 demandes partielles devraient être assemblées d'abord de toute façon, donc une implémentation multithread naïve ne fonctionnera pas dans les deux sens.
- Ne fonctionne pas comme on pourrait s'y attendre avec la lecture/écriture de fichiers ("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 sontnottrue, epoll peut être inefficace et
poll
peut fonctionner de manière égale ou meilleure. epoll
ne peut pas faire de "magie", c'est-à-dire qu'il est toujours nécessairement O(N) par rapport au nombre d'événementsqui se produisent.- Cependant,
epoll
fonctionne bien avec le nouvel appel système recvmmsg
, car il renvoie plusieurs notifications de disponibilité à la fois (autant que possible, jusqu'à ce que vous spécifiez comme maxevents
) . Cela permet de recevoir par ex. 15 notifications EPOLLIN avec un syscall sur un serveur occupé et lisez les 15 messages correspondants avec un second syscall (une réduction de 93% des syscalls!). Malheureusement, toutes les opérations sur un appel recvmmsg
se réfèrent à la même socket, donc c'est surtout utile pour les services basés sur UDP (pour TCP, il devrait y avoir une sorte d'appel système recvmmsmsg
qui prend également un descripteur de socket par article!). - Les descripteurs doiventtoujoursêtre définis sur non bloquants et il faut vérifier
EAGAIN
même en utilisant epoll
car il existe des situations exceptionnelles où epoll
signale l'état de préparation et une lecture (ou écriture) ultérieure serastillblock. C'est également le cas pour poll
/select
sur certains noyaux (bien qu'il ait probablement été corrigé). - Avec une implémentation denaive, la famine des expéditeurs lents est possible. Lorsque vous lisez à l'aveugle jusqu'à ce que
EAGAIN
soit renvoyé lors de la réception d'une notification, il est possible de lire indéfiniment de nouvelles données entrantes d'un expéditeur rapide tout en affamant complètement un expéditeur lent (tant que les données continuent à arriver assez rapidement, vous pourriez ne pas voir EAGAIN
pendant un bon moment!). S'applique à poll
/select
de la même manière. - Le mode déclenché par le bord présente certaines bizarreries 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 une activité IO 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 Edge est beaucoup plus proche de "réveille le threadpremierqui a appelé _epoll_wait
_, signalant que IO une activité s'est produite depuisn'importe quidernier appelsoit_epoll_wait
_ouune fonction de lecture/écriture sur le descripteur, puis ne signale à nouveau que l'état de préparationau thread suivant appelant ou déjà bloqué dans_epoll_wait
_, pour toutes les opérations se produisant aprèsn'importe quiappelé une fonction de lecture (ou d'écriture) sur le descripteur ". Cela a également du sens ... ce n'est tout simplement pas exactement ce que la documentation suggère.
kqueue
- Analogue BSD à
epoll
, utilisation différente, effet similaire. - Fonctionne également sur Mac OS X
- Selon les rumeurs, être plus rapide (je ne l'ai jamais utilisé, je ne peux donc 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 d'E/S
- Epoll pour Windows, ou plutôt epoll sur les stéroïdes.
- Fonctionne de manière transparente avectoutqui peut être attendu ou alerté d'une manière ou d'une autre (sockets, temporisateurs attendus, opérations sur les fichiers, threads, processus)
- Si Microsoft a bien compris une chose dans Windows, ce sont les ports d'achèvement:
- Fonctionne sans souci avec n'importe quel nombre de threads
- Pas de troupeau tonitruant
- Réveille les threads un par un dans un ordre LIFO
- Maintient les caches au chaud et minimise les changements de contexte
- Respecte le nombre de processeurs sur la machine ou fournit le nombre souhaité de travailleurs
- Permet à l'application de publier des événements, ce qui se prête à une mise en œuvre de file d'attente de travail parallèle très simple, sécurisée et efficace (planifie jusqu'à 500 000 tâches par seconde sur mon système).
- Inconvénient mineur: ne supprime pas facilement les descripteurs de fichiers une fois ajoutés (doit fermer et rouvrir).
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 pas plus loin: vous l'avez déjà disponible en boost-asio.
Des suggestions de tutoriels simples/basiques?
Les cadres répertoriés ci-dessus sont fournis avec une documentation complète. Linux docs et MSDN explique 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 de complétion IO (notez que vous appelez CreateIoCompletionPort deux fois avec des paramètres 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.)
ÉDITER:
Notez que les ports de complétion (Windows) fonctionnent conceptuellement dans le sens inverse comme epoll (ou kqueue). Ils signalent, comme leur nom l'indique,achèvement, pasdisponibilité. Autrement dit, vous déclenchez une demande asynchrone et l'oubliez jusqu'à un certain temps plus tard, on vous dit qu'elle s'est terminée (avec succès ou pas tant de succès, et il y a le cas exceptionnel de "terminé immédiatement" aussi).
Avec epoll, vous bloquez jusqu'à ce que vous soyez averti que "certaines données" (peut-être 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 bloquer. Ce n'est qu'alors que vous démarrez l'opération réelle, qui alors, espérons-le, ne bloquera pas (à part ce que vous attendez, il n'y a aucune garantie stricte pour cela - c'est donc une bonne idée de définir des descripteurs sur non bloquant et de vérifier EAGAIN [EAGAINetEWOULDBLOCK pour les sockets, car oh joie, la norme autorise deux valeurs d'erreur différentes]).