Was ist der Unterschied zwischen Epoll, Poll, Threadpool?


75

Könnte jemand erklären , was der Unterschied zwischen ist epoll, pollund Threadpool ?

  • Was sind die Vor- und Nachteile?
  • Anregungen für Frameworks?
  • Anregungen für einfache / grundlegende Tutorials?
  • Es scheint, dass epollund pollsind Linux-spezifisch ... Gibt es eine gleichwertige Alternative für Windows?

Antworten:


216

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.
      • futexist dein Freund hier, in Kombination mit zB einer Schnellvorlaufwarteschlange pro Thread. Obwohl schlecht dokumentiert und unhandlich, futexbietet es genau das, was benötigt wird. epollkann mehrere Ereignisse gleichzeitig zurückgeben und futexermö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 forkist 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 forknegativ 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 pollfunktioniert
        • O (1) mit kleinem k (sehr schnell) in Bezug auf die Anzahl der Deskriptoren anstelle von O (n)
    • Funktioniert sehr gut mit timerfdund 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 polleine gleichwertige oder bessere Leistung erbringen.
      • epollkann nicht "zaubern", dh es ist immer noch notwendigerweise O (N) in Bezug auf die Anzahl der Ereignisse, die auftreten .
      • Funktioniert jedoch epollgut mit dem neuen recvmmsgSystemaufruf, 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 recvmmsgbeziehen 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 recvmmsmsgSystemaufruf geben, für den auch ein Socket-Deskriptor pro Element erforderlich ist!).
      • Deskriptoren sollten immer auf nicht blockierend eingestellt sein und EAGAINauch bei Verwendung überprüft werden, epollda es Ausnahmesituationen gibt, in denen die epollBerichtsbereitschaft und ein nachfolgendes Lesen (oder Schreiben) weiterhin blockiert werden. Dies gilt auch für poll/ selectauf einigen Kerneln (obwohl dies vermutlich behoben wurde).
      • Mit einer naiven Implementierung ist das Verhungern langsamer Absender möglich. Wenn Sie blind lesen, bis EAGAINnach 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 EAGAINeine ganze Weile nicht sehen ! ). Gilt für poll/ selectin 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]).


1
Ich bin nicht einverstanden mit Ihrer Aussage, dass E / A-Abschlussports eine Sache sind, die MS richtig gemacht hat. Ich bin froh, dass du das Rückwärtsdesign in der Bearbeitung bemerkt hast!
Matt

Schöne Antwort (+1). Aber meintest du min(num_cpu, num_events)in der "Futex" -Beschreibung?
Nemo

@Nemo: Du hast natürlich recht, muss sein min, nicht max- ich werde den Tippfehler beheben. Vielen Dank.
Damon

1
Eigentlich habe ich meine Meinung dazu etwas geändert. Nach der Arbeit mit RDMA passt die IOCP-API besser zu diesem Modell. Möglicherweise ist die Leistung besser. In der Praxis bin ich mir nicht so sicher. Wie auch immer ... Ich würde nicht sagen, dass es rückwärts ist, nur anders und viel schwieriger, den Kopf herumzukriegen.
Matt

Ich mag alle Details, die Sie zur Verfügung gestellt haben. Ich denke, dass EPOLLET immer noch alle Fäden weckt. fs / eventpoll.c: ep_send_events_proc () ist die einzige Funktion, die dieses Flag verwendet und nur bestimmt, ob es wieder in die Bereitschaftsliste eingefügt werden soll.
Ant Manelope
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.