Threadpool passt nicht wirklich in dieselbe Kategorie wie Poll und Epoll, daher gehe ich davon aus, dass Sie sich auf Threadpool beziehen, wie in "Threadpool, um viele Verbindungen mit einem Thread pro Verbindung zu verarbeiten".
Vor-und Nachteile
- Threadpool
- Angemessen effizient für kleine und mittlere Parallelität, kann sogar andere Techniken übertreffen.
- Verwendet mehrere Kerne.
- Skaliert nicht weit über "mehrere Hundert" hinaus, obwohl einige Systeme (z. B. Linux) im Prinzip 100.000 Threads einplanen können.
- Die naive Implementierung weist das Problem der " donnernden Herde " auf.
- Abgesehen von Kontextwechsel und donnernder Herde muss man das Gedächtnis berücksichtigen. Jeder Thread hat einen Stapel (normalerweise mindestens ein Megabyte). Tausend Threads benötigen daher ein Gigabyte RAM nur für den Stapel. Selbst wenn dieser Speicher nicht festgeschrieben ist, nimmt er unter einem 32-Bit-Betriebssystem immer noch beträchtlichen Adressraum weg (unter 64-Bit kein wirkliches Problem).
- Threads können tatsächlich verwendet werden
epoll
, obwohl der offensichtliche Weg (alle Threads blockieren epoll_wait
) keinen Nutzen hat, da epoll jeden darauf wartenden Thread aufweckt und daher immer noch dieselben Probleme aufweist.
- Optimale Lösung: Ein einzelner Thread lauscht auf Epoll, führt das Eingangsmultiplexing durch und übergibt vollständige Anforderungen an einen Threadpool.
futex
ist dein Freund hier, in Kombination mit zB einer Schnellvorlaufwarteschlange pro Thread. Obwohl schlecht dokumentiert und unhandlich, futex
bietet es genau das, was benötigt wird. epoll
kann mehrere Ereignisse gleichzeitig zurückgeben und futex
ermöglicht es Ihnen, N blockierte Threads gleichzeitig effizient und präzise zu aktivieren ( min(num_cpu, num_events)
idealerweise N ), und im besten Fall ist überhaupt kein zusätzlicher Systemaufruf / Kontextwechsel erforderlich.
- Nicht trivial zu implementieren, ist vorsichtig.
fork
(auch bekannt als Old Fashion Threadpool)
- Ziemlich effizient für kleine und mittlere Parallelität.
- Skaliert nicht weit über "wenige Hundert" hinaus.
- Kontextwechsel sind viel teurer (unterschiedliche Adressräume!).
- Skaliert auf älteren Systemen, auf denen Gabel viel teurer ist, erheblich schlechter (tiefe Kopie aller Seiten). Selbst auf modernen Systemen
fork
ist dies nicht "kostenlos", obwohl der Overhead meist durch den Copy-on-Write-Mechanismus zusammengeführt wird. Bei großen Datenmengen, die ebenfalls geändert werden , kann sich eine beträchtliche Anzahl von Seitenfehlern fork
negativ auf die Leistung auswirken.
- Bewährt sich jedoch seit über 30 Jahren als zuverlässig.
- Lächerlich einfach zu implementieren und absolut solide: Wenn einer der Prozesse abstürzt, endet die Welt nicht. Es gibt (fast) nichts, was man falsch machen kann.
- Sehr anfällig für "donnernde Herde".
poll
/. select
- Zwei Geschmacksrichtungen (BSD vs. System V), die mehr oder weniger dasselbe sind.
- Etwas alt und langsam, etwas umständlich, aber es gibt praktisch keine Plattform, die sie nicht unterstützt.
- Wartet, bis auf einer Reihe von Deskriptoren "etwas passiert"
- Ermöglicht einem Thread / Prozess, mehrere Anforderungen gleichzeitig zu verarbeiten.
- Keine Multi-Core-Nutzung.
- Muss die Liste der Deskriptoren jedes Mal, wenn Sie warten, vom Benutzer in den Kernelbereich kopieren. Muss eine lineare Suche über Deskriptoren durchführen. Dies schränkt seine Wirksamkeit ein.
- Skaliert nicht gut auf "Tausende" (in der Tat harte Grenze um 1024 auf den meisten Systemen oder so niedrig wie 64 auf einigen).
- Verwenden Sie es, weil es portabel ist, wenn Sie ohnehin nur mit einem Dutzend Deskriptoren arbeiten (keine Leistungsprobleme) oder wenn Sie Plattformen unterstützen müssen, die nichts Besseres haben. Nicht anders verwenden.
- Konzeptionell wird ein Server etwas komplizierter als ein gegabelter Server, da Sie jetzt für jede Verbindung viele Verbindungen und eine Zustandsmaschine verwalten müssen und zwischen eingehenden Anforderungen multiplexen, Teilanforderungen zusammenstellen usw. müssen. Ein einfacher gegabelter Server Der Server kennt nur einen einzelnen Socket (also zwei, der den Listening-Socket zählt), liest, bis er das hat, was er will, oder bis die Verbindung halb geschlossen ist, und schreibt dann, was er will. Es geht nicht um Blockierung, Bereitschaft oder Hunger oder um eingehende Daten, das ist das Problem eines anderen Prozesses.
epoll
- Nur Linux.
- Konzept teurer Modifikationen vs. effizienter Wartezeiten:
- Kopiert Informationen zu Deskriptoren in den Kernelbereich, wenn Deskriptoren hinzugefügt werden (
epoll_ctl
)
- Dies ist normalerweise etwas, was selten vorkommt .
- Muss keine Daten in den Kernel-Speicher kopieren, wenn auf Ereignisse gewartet wird (
epoll_wait
)
- Dies ist normalerweise etwas, das sehr oft passiert .
- Fügt den Kellner (oder besser gesagt seine Epoll-Struktur) zu den Warteschlangen der Deskriptoren hinzu
- Der Deskriptor weiß daher, wer zuhört, und signalisiert den Kellnern bei Bedarf direkt, anstatt dass die Kellner eine Liste von Deskriptoren durchsuchen
- Gegenteil wie es
poll
funktioniert
- O (1) mit kleinem k (sehr schnell) in Bezug auf die Anzahl der Deskriptoren anstelle von O (n)
- Funktioniert sehr gut mit
timerfd
und eventfd
(atemberaubende Timer-Auflösung und Genauigkeit auch).
- Funktioniert gut mit
signalfd
, eliminiert den umständlichen Umgang mit Signalen und macht sie auf sehr elegante Weise Teil des normalen Kontrollflusses.
- Eine Epoll-Instanz kann andere Epoll-Instanzen rekursiv hosten
- Annahmen dieses Programmiermodells:
- Die meisten Deskriptoren sind die meiste Zeit im Leerlauf, nur wenige Dinge (z. B. "Daten empfangen", "Verbindung geschlossen") passieren tatsächlich bei wenigen Deskriptoren.
- Meistens möchten Sie keine Deskriptoren zum Set hinzufügen oder daraus entfernen.
- Meistens warten Sie darauf, dass etwas passiert.
- Einige kleinere Fallstricke:
- Ein Level-ausgelöster Epoll weckt alle darauf wartenden Threads (dies funktioniert "wie beabsichtigt"), daher ist die naive Art, Epoll mit einem Threadpool zu verwenden, nutzlos. Zumindest für einen TCP-Server ist dies kein großes Problem, da Teilanforderungen ohnehin zuerst zusammengestellt werden müssten, sodass eine naive Multithread-Implementierung in keiner Weise funktioniert.
- Funktioniert nicht wie erwartet beim Lesen / Schreiben von Dateien ("immer bereit").
- Konnte bis vor kurzem nicht mit AIO verwendet werden, jetzt über möglich
eventfd
, erfordert jedoch eine (bisher) undokumentierte Funktion.
- Wenn die obigen Annahmen nicht zutreffen, kann epoll ineffizient sein und
poll
eine gleichwertige oder bessere Leistung erbringen.
epoll
kann nicht "zaubern", dh es ist immer noch notwendigerweise O (N) in Bezug auf die Anzahl der Ereignisse, die auftreten .
- Funktioniert jedoch
epoll
gut mit dem neuen recvmmsg
Systemaufruf, da mehrere Bereitschaftsbenachrichtigungen gleichzeitig zurückgegeben werden (so viele wie verfügbar, bis zu dem, was Sie angeben maxevents
). Dies ermöglicht es, z. B. 15 EPOLLIN-Benachrichtigungen mit einem Systemaufruf auf einem ausgelasteten Server zu empfangen und die entsprechenden 15 Nachrichten mit einem zweiten Systemaufruf zu lesen (eine Reduzierung der Systemaufrufe um 93%!). Leider recvmmsg
beziehen sich alle Operationen auf einen Aufruf auf denselben Socket, daher ist dies hauptsächlich für UDP-basierte Dienste nützlich (für TCP müsste es eine Art recvmmsmsg
Systemaufruf geben, für den auch ein Socket-Deskriptor pro Element erforderlich ist!).
- Deskriptoren sollten immer auf nicht blockierend eingestellt sein und
EAGAIN
auch bei Verwendung überprüft werden, epoll
da es Ausnahmesituationen gibt, in denen die epoll
Berichtsbereitschaft und ein nachfolgendes Lesen (oder Schreiben) weiterhin blockiert werden. Dies gilt auch für poll
/ select
auf einigen Kerneln (obwohl dies vermutlich behoben wurde).
- Mit einer naiven Implementierung ist das Verhungern langsamer Absender möglich. Wenn Sie blind lesen, bis
EAGAIN
nach Erhalt einer Benachrichtigung zurückgegeben wird, können Sie neue eingehende Daten von einem schnellen Absender auf unbestimmte Zeit lesen, während Sie einen langsamen Absender vollständig aushungern lassen (solange die Daten schnell genug eingehen, werden Sie sie möglicherweise EAGAIN
eine ganze Weile nicht sehen ! ). Gilt für poll
/ select
in gleicher Weise.
- Der durch Kanten ausgelöste Modus weist in einigen Situationen einige Macken und unerwartetes Verhalten auf, da die Dokumentation (sowohl Manpages als auch TLPI) vage ("wahrscheinlich", "sollte", "könnte") und manchmal irreführend in Bezug auf ihre Funktionsweise ist.
Die Dokumentation besagt, dass mehrere Threads, die auf einen Epoll warten, alle signalisiert werden. Außerdem wird in einer Benachrichtigung angegeben, ob seit dem letzten Aufruf von epoll_wait
(oder seit dem Öffnen des Deskriptors, wenn kein vorheriger Aufruf stattgefunden hat) eine E / A-Aktivität stattgefunden hat .
Das wahre, beobachtbares Verhalten im flankengetriggerten Modus ist viel näher an „weckt den ersten Thread, der angerufen hat epoll_wait
, signalisiert , dass IO Aktivität seit passiert jeden zuletzt genannte entweder epoll_wait
odereine Lese- / Schreibfunktion für den Deskriptor und meldet danach nur die Bereitschaft erneut an den nächsten aufrufenden oder bereits blockierten Thread epoll_wait
für alle Operationen, die ausgeführt werden, nachdem jemand eine Lese- (oder Schreib-) Funktion für den Deskriptor aufgerufen hat ". Dies ist irgendwie sinnvoll Auch ... es ist einfach nicht genau das, was die Dokumentation vorschlägt.
kqueue
- BSD-Analogie zu
epoll
, unterschiedlicher Verwendung, ähnlicher Effekt.
- Funktioniert auch unter Mac OS X.
- Gerüchten zufolge schneller (ich habe es nie benutzt, kann also nicht sagen, ob das stimmt).
- Registriert Ereignisse und gibt eine Ergebnismenge in einem einzigen Systemaufruf zurück.
- E / A-Abschlussports
- Epoll für Windows oder besser Epoll für Steroide.
- Funktioniert nahtlos mit allem , was auf irgendeine Weise wartbar oder alarmierbar ist (Sockets, wartbare Timer, Dateivorgänge, Threads, Prozesse).
- Wenn Microsoft in Windows eines richtig gemacht hat, sind es die Abschlussports:
- Funktioniert problemlos mit einer beliebigen Anzahl von Threads
- Keine donnernde Herde
- Weckt Threads einzeln in einer LIFO-Reihenfolge auf
- Hält die Caches warm und minimiert Kontextwechsel
- Respektiert die Anzahl der Prozessoren auf der Maschine oder liefert die gewünschte Anzahl von Arbeitern
- Ermöglicht der Anwendung das Posten von Ereignissen, was sich für eine sehr einfache, ausfallsichere und effiziente Implementierung einer parallelen Arbeitswarteschlange eignet (plant auf meinem System mehr als 500.000 Aufgaben pro Sekunde).
- Kleiner Nachteil: Entfernt Dateideskriptoren nach dem Hinzufügen nicht einfach (muss geschlossen und erneut geöffnet werden).
Frameworks
libevent - Die Version 2.0 unterstützt auch Abschlussports unter Windows.
ASIO - Wenn Sie Boost in Ihrem Projekt verwenden, suchen Sie nicht weiter: Sie haben dies bereits als Boost-Asio verfügbar.
Anregungen für einfache / grundlegende Tutorials?
Die oben aufgeführten Frameworks werden mit einer umfassenden Dokumentation geliefert. In den Linux- Dokumenten und im MSDN werden Epoll- und Vervollständigungsports ausführlich erläutert.
Mini-Tutorial zur Verwendung von 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-Tutorial für E / A-Abschlussports (Hinweis: Rufen Sie CreateIoCompletionPort zweimal mit unterschiedlichen Parametern auf):
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();
(Diese Mini-Tuts lassen jede Art von Fehlerprüfung aus, und ich hoffe, ich habe keine Tippfehler gemacht, aber sie sollten größtenteils in Ordnung sein, um Ihnen eine Vorstellung zu geben.)
BEARBEITEN:
Beachten Sie, dass Vervollständigungsports (Windows) konzeptionell umgekehrt als epoll (oder kqueue) funktionieren. Sie signalisieren, wie der Name schon sagt, Vollendung , nicht Bereitschaft . Das heißt, Sie lösen eine asynchrone Anforderung aus und vergessen sie, bis Ihnen einige Zeit später mitgeteilt wird, dass sie abgeschlossen wurde (entweder erfolgreich oder nicht so erfolgreich, und es gibt den Ausnahmefall "sofort abgeschlossen").
Mit epoll blockieren Sie, bis Sie benachrichtigt werden, dass entweder "einige Daten" (möglicherweise nur ein Byte) eingetroffen sind und verfügbar sind oder genügend Pufferplatz vorhanden ist, damit Sie einen Schreibvorgang ausführen können, ohne zu blockieren. Erst dann starten Sie den eigentlichen Vorgang, der dann hoffentlich nicht blockiert wird (anders als erwartet gibt es dafür keine strikte Garantie - es ist daher eine gute Idee, Deskriptoren auf nicht blockierend zu setzen und nach EAGAIN [EAGAIN und EWOULDBLOCK zu suchen für Steckdosen, denn oh Freude, der Standard erlaubt zwei verschiedene Fehlerwerte]).