Ich habe im Internet nach technischen Details zum Blockieren von E / A und nicht blockierenden E / A gesucht und mehrere Personen gefunden, die angaben, dass nicht blockierende E / A schneller sind als blockierende E / A. Zum Beispiel in diesem Dokument .
Wenn ich blockierende E / A verwende, kann der aktuell blockierte Thread natürlich nichts anderes tun ... weil er blockiert ist. Sobald jedoch ein Thread blockiert wird, kann das Betriebssystem zu einem anderen Thread wechseln und erst wieder zurückwechseln, wenn für den blockierten Thread etwas zu tun ist. Solange es einen anderen Thread auf dem System gibt, der CPU benötigt und nicht blockiert ist, sollte im Vergleich zu einem ereignisbasierten nicht blockierenden Ansatz keine CPU-Leerlaufzeit mehr vorhanden sein.
Neben der Reduzierung der CPU-Leerlaufzeit sehe ich eine weitere Option, um die Anzahl der Aufgaben zu erhöhen, die ein Computer in einem bestimmten Zeitraum ausführen kann: Reduzieren Sie den durch das Wechseln von Threads verursachten Overhead. Aber wie geht das? Und ist der Overhead groß genug, um messbare Effekte zu zeigen? Hier ist eine Idee, wie ich mir vorstellen kann, wie es funktioniert:
- Um den Inhalt einer Datei zu laden, delegiert eine Anwendung diese Aufgabe an ein ereignisbasiertes E / A-Framework und übergibt eine Rückruffunktion zusammen mit einem Dateinamen
- Das Ereignisframework wird an das Betriebssystem delegiert, das einen DMA-Controller der Festplatte so programmiert, dass die Datei direkt in den Speicher geschrieben wird
- Das Ereignis-Framework ermöglicht die Ausführung von weiterem Code.
- Nach Abschluss der Disk-to-Memory-Kopie verursacht der DMA-Controller einen Interrupt.
- Der Interrupt-Handler des Betriebssystems benachrichtigt das ereignisbasierte E / A-Framework darüber, dass die Datei vollständig in den Speicher geladen wird. Wie macht es das? Mit einem Signal?
- Der Code, der derzeit im Ereignis-E / A-Framework ausgeführt wird, wird beendet.
- Das ereignisbasierte E / A-Framework überprüft seine Warteschlange, erkennt die Nachricht des Betriebssystems aus Schritt 5 und führt den in Schritt 1 erhaltenen Rückruf aus.
Funktioniert das so? Wenn nicht, wie funktioniert es? Das bedeutet, dass das Ereignissystem funktionieren kann, ohne jemals den Stapel explizit berühren zu müssen (z. B. ein echter Scheduler, der den Stapel sichern und den Stapel eines anderen Threads in den Speicher kopieren müsste, während er den Thread wechselt)? Wie viel Zeit spart dies tatsächlich? Ist da noch mehr dran?
quelle
Antworten:
Der größte Vorteil von nicht blockierenden oder asynchronen E / A besteht darin, dass Ihr Thread seine Arbeit parallel fortsetzen kann. Dies können Sie natürlich auch mit einem zusätzlichen Thread erreichen. Wie Sie für die beste Gesamtleistung (Systemleistung) angegeben haben, ist es wahrscheinlich besser, asynchrone E / A und nicht mehrere Threads zu verwenden (wodurch die Thread-Umschaltung verringert wird).
Schauen wir uns mögliche Implementierungen eines Netzwerkserverprogramms an, das 1000 parallel verbundene Clients verarbeiten soll:
Jeder Thread benötigt Speicherressourcen (auch Kernelspeicher!), Das ist ein Nachteil. Und jeder zusätzliche Thread bedeutet mehr Arbeit für den Scheduler.
Dies entlastet das System, da wir weniger Threads haben. Es verhindert jedoch auch, dass Sie die volle Leistung Ihres Computers nutzen können, da Sie möglicherweise einen Prozessor zu 100% fahren und alle anderen Prozessoren im Leerlauf laufen lassen.
Dies entlastet das System, da weniger Threads vorhanden sind. Und es können alle verfügbaren Prozessoren verwendet werden. Unter Windows wird dieser Ansatz von der Thread Pool API unterstützt .
Natürlich ist es per se kein Problem, mehr Threads zu haben. Wie Sie vielleicht erkannt haben, habe ich eine große Anzahl von Verbindungen / Threads ausgewählt. Ich bezweifle, dass Sie einen Unterschied zwischen den drei möglichen Implementierungen feststellen werden, wenn es sich nur um ein Dutzend Threads handelt (dies schlägt Raymond Chen auch im MSDN-Blogbeitrag vor. Hat Windows ein Limit von 2000 Threads pro Prozess? ).
Unter Windows bedeutet die Verwendung ungepufferter Datei-E / A , dass Schreibvorgänge eine Größe haben müssen, die ein Vielfaches der Seitengröße beträgt. Ich habe es nicht getestet, aber es scheint, dass dies auch die Schreibleistung für gepufferte synchrone und asynchrone Schreibvorgänge positiv beeinflussen könnte.
Die Schritte 1 bis 7, die Sie beschreiben, geben eine gute Vorstellung davon, wie es funktioniert. Unter Windows informiert Sie das Betriebssystem über den Abschluss einer asynchronen E / A (
WriteFile
mitOVERLAPPED
Struktur) mithilfe eines Ereignisses oder eines Rückrufs. Rückruffunktionen werden beispielsweise nur aufgerufen, wenn Ihr CodeWaitForMultipleObjectsEx
mitbAlertable
set auf aufrufttrue
.Noch etwas im Internet lesen:
quelle
E / A umfasst verschiedene Arten von Vorgängen wie das Lesen und Schreiben von Daten von Festplatten, den Zugriff auf Netzwerkressourcen, das Aufrufen von Webdiensten oder das Abrufen von Daten aus Datenbanken. Abhängig von der Plattform und der Art des Vorgangs nutzt die asynchrone E / A normalerweise Hardware- oder Low-Level-Systemunterstützung für die Ausführung des Vorgangs. Dies bedeutet, dass die CPU so wenig wie möglich belastet wird.
Auf Anwendungsebene verhindert asynchrone E / A, dass Threads auf den Abschluss von E / A-Vorgängen warten müssen. Sobald eine asynchrone E / A-Operation gestartet wird, gibt sie den Thread frei, auf dem sie gestartet wurde, und ein Rückruf wird registriert. Wenn der Vorgang abgeschlossen ist, wird der Rückruf zur Ausführung auf dem ersten verfügbaren Thread in die Warteschlange gestellt.
Wenn die E / A-Operation synchron ausgeführt wird, wird der laufende Thread so lange nicht ausgeführt, bis die Operation abgeschlossen ist. Die Laufzeit weiß nicht, wann der E / A-Vorgang abgeschlossen ist. Daher stellt sie dem wartenden Thread regelmäßig CPU-Zeit zur Verfügung. Diese CPU-Zeit könnte andernfalls von anderen Threads verwendet werden, für die tatsächlich CPU-gebundene Vorgänge ausgeführt werden müssen.
Wie @ user1629468 erwähnt, bietet asynchrone E / A keine bessere Leistung, sondern eine bessere Skalierbarkeit. Dies ist offensichtlich, wenn Sie in Kontexten ausgeführt werden, in denen nur eine begrenzte Anzahl von Threads verfügbar ist, wie dies bei Webanwendungen der Fall ist. Webanwendungen verwenden normalerweise einen Thread-Pool, aus dem sie jeder Anforderung Threads zuweisen. Wenn Anforderungen bei lang laufenden E / A-Vorgängen blockiert werden, besteht die Gefahr, dass der Webpool erschöpft wird und die Webanwendung einfriert oder nur langsam reagiert.
Eine Sache, die mir aufgefallen ist, ist, dass asynchrone E / A nicht die beste Option ist, wenn sehr schnelle E / A-Vorgänge ausgeführt werden. In diesem Fall ist der Vorteil, einen Thread nicht beschäftigt zu halten, während auf den Abschluss der E / A-Operation gewartet wird, nicht sehr wichtig, und die Tatsache, dass die Operation auf einem Thread gestartet und auf einem anderen abgeschlossen wird, erhöht die Gesamtausführung um einen Overhead.
Eine detailliertere Untersuchung, die ich kürzlich zum Thema asynchrone E / A vs. Multithreading durchgeführt habe, finden Sie hier .
quelle
Der Hauptgrund für die Verwendung von AIO liegt in der Skalierbarkeit. Im Kontext einiger Themen sind die Vorteile nicht offensichtlich. Wenn das System jedoch auf 1000 Threads skaliert, bietet AIO eine viel bessere Leistung. Die Einschränkung ist, dass die AIO-Bibliothek keine weiteren Engpässe verursachen sollte.
quelle
Um eine Geschwindigkeitsverbesserung aufgrund einer Form von Multi-Computing zu vermuten, müssen Sie entweder davon ausgehen, dass mehrere CPU-basierte Aufgaben gleichzeitig auf mehreren Computerressourcen (im Allgemeinen Prozessorkerne) ausgeführt werden oder dass nicht alle Aufgaben auf der gleichzeitigen Verwendung von basieren Dieselbe Ressource - das heißt, einige Aufgaben hängen möglicherweise von einer Systemunterkomponente ab (z. B. Festplattenspeicher), während einige Aufgaben von einer anderen abhängen (Empfang von Kommunikation von einem Peripheriegerät), und andere erfordern möglicherweise die Verwendung von Prozessorkernen.
Das erste Szenario wird oft als "parallele" Programmierung bezeichnet. Das zweite Szenario wird häufig als "gleichzeitige" oder "asynchrone" Programmierung bezeichnet, obwohl "gleichzeitig" manchmal auch verwendet wird, um den Fall zu bezeichnen, dass ein Betriebssystem lediglich die Ausführung mehrerer Aufgaben verschachteln kann, unabhängig davon, ob eine solche Ausführung erforderlich ist seriell platzieren oder wenn mehrere Ressourcen verwendet werden können, um eine parallele Ausführung zu erreichen. In diesem letzteren Fall bezieht sich "gleichzeitig" im Allgemeinen auf die Art und Weise, wie die Ausführung in das Programm geschrieben wird, und nicht auf die Perspektive der tatsächlichen Gleichzeitigkeit der Aufgabenausführung.
Es ist sehr einfach, mit stillschweigenden Annahmen darüber zu sprechen. Einige behaupten beispielsweise schnell: "Asynchrone E / A sind schneller als Multithread-E / A." Diese Behauptung ist aus mehreren Gründen zweifelhaft. Erstens könnte es vorkommen, dass einige gegebene asynchrone E / A-Frameworks genau mit Multithreading implementiert werden. In diesem Fall sind sie ein und dasselbe und es macht keinen Sinn zu sagen, dass ein Konzept "schneller als" das andere ist .
Zweitens müssen Sie auch dann, wenn eine Single-Threaded-Implementierung eines asynchronen Frameworks (z. B. einer Single-Threaded-Ereignisschleife) vorhanden ist, eine Annahme darüber treffen, was diese Schleife tut. Eine dumme Sache, die Sie mit einer Single-Threaded-Ereignisschleife tun können, ist beispielsweise die Anforderung, zwei verschiedene rein CPU-gebundene Aufgaben asynchron auszuführen. Wenn Sie dies auf einem Computer mit nur einem idealisierten Einzelprozessorkern tun würden (ohne Berücksichtigung moderner Hardwareoptimierungen), würde die Ausführung dieser Aufgabe "asynchron" nicht anders ablaufen als mit zwei unabhängig verwalteten Threads oder mit nur einem einzigen Prozess. - Der Unterschied kann auf das Umschalten des Thread-Kontexts oder die Optimierung des Betriebssystemplans zurückzuführen sein. Wenn jedoch beide Aufgaben an die CPU gehen, ist dies in beiden Fällen ähnlich.
Es ist nützlich, sich viele der ungewöhnlichen oder dummen Eckfälle vorzustellen, auf die Sie stoßen könnten.
"Asynchron" muss nicht gleichzeitig sein, zum Beispiel wie oben: Sie "asynchron" führen zwei CPU-gebundene Tasks auf einem Computer mit genau einem Prozessorkern aus.
Die Ausführung mit mehreren Threads muss nicht gleichzeitig erfolgen: Sie erzeugen zwei Threads auf einem Computer mit einem einzelnen Prozessorkern oder fordern zwei Threads auf, eine andere Art von knapper Ressource zu erwerben (stellen Sie sich beispielsweise eine Netzwerkdatenbank vor, die nur einen einrichten kann Verbindung zu einem Zeitpunkt). Die Ausführung der Threads kann verschachtelt sein, wie es der Scheduler des Betriebssystems für richtig hält, aber ihre Gesamtlaufzeit kann nicht auf einem einzelnen Kern reduziert werden (und wird durch das Wechseln des Thread-Kontexts erhöht) (oder allgemeiner, wenn Sie mehr Threads erzeugen als vorhanden Kerne, um sie auszuführen, oder mehr Threads, die nach einer Ressource fragen, als die Ressource aushalten kann). Das Gleiche gilt auch für die Mehrfachverarbeitung.
Daher müssen weder asynchrone E / A noch Multithreading einen Leistungsgewinn in Bezug auf die Laufzeit bieten. Sie können sogar die Dinge verlangsamen.
Wenn Sie jedoch einen bestimmten Anwendungsfall definieren, z. B. ein bestimmtes Programm, das sowohl einen Netzwerkaufruf zum Abrufen von Daten von einer mit dem Netzwerk verbundenen Ressource wie einer entfernten Datenbank ausführt als auch eine lokale CPU-gebundene Berechnung durchführt, können Sie anfangen, darüber nachzudenken Die Leistungsunterschiede zwischen den beiden Methoden unter einer bestimmten Annahme über die Hardware.
Die zu stellenden Fragen: Wie viele Rechenschritte muss ich ausführen und wie viele unabhängige Ressourcensysteme gibt es, um sie auszuführen? Gibt es Teilmengen der Rechenschritte, die die Verwendung unabhängiger Systemunterkomponenten erfordern und davon gleichzeitig profitieren können? Wie viele Prozessorkerne habe ich und wie hoch ist der Aufwand für die Verwendung mehrerer Prozessoren oder Threads, um Aufgaben auf separaten Kernen auszuführen?
Wenn Ihre Aufgaben weitgehend von unabhängigen Subsystemen abhängen, ist eine asynchrone Lösung möglicherweise hilfreich. Wenn die Anzahl der für die Verarbeitung erforderlichen Threads groß wäre, sodass die Kontextumschaltung für das Betriebssystem nicht mehr trivial wäre, ist eine asynchrone Lösung mit einem Thread möglicherweise besser.
Immer wenn die Aufgaben an dieselbe Ressource gebunden sind (z. B. müssen mehrere gleichzeitig auf dasselbe Netzwerk oder dieselbe lokale Ressource zugreifen), führt Multithreading wahrscheinlich zu einem unbefriedigenden Overhead, und während Single-Threaded-Asynchronität in einer solchen Ressource möglicherweise weniger Overhead verursacht. begrenzte Situation kann es auch keine Beschleunigung erzeugen. In einem solchen Fall besteht die einzige Option (wenn Sie eine Beschleunigung wünschen) darin, mehrere Kopien dieser Ressource verfügbar zu machen (z. B. mehrere Prozessorkerne, wenn die knappe Ressource CPU ist; eine bessere Datenbank, die mehr gleichzeitige Verbindungen unterstützt, wenn die knappe Ressource vorhanden ist ist eine verbindungsbeschränkte Datenbank usw.).
Eine andere Möglichkeit ist: Das Betriebssystem kann die Verwendung einer einzelnen Ressource für zwei Aufgaben verschachteln. Dies kann nicht schneller sein, als nur eine Aufgabe die Ressource verwenden zu lassen, während die andere wartet, und dann die zweite Aufgabe seriell zu beenden. Darüber hinaus bedeuten die Scheduler-Kosten für das Interleaving, dass in jeder realen Situation tatsächlich eine Verlangsamung auftritt. Es spielt keine Rolle, ob die verschachtelte Nutzung der CPU, einer Netzwerkressource, einer Speicherressource, eines Peripheriegeräts oder einer anderen Systemressource erfolgt.
quelle
Eine mögliche Implementierung von nicht blockierenden E / A ist genau das, was Sie gesagt haben, mit einem Pool von Hintergrund-Threads, die E / A blockieren und den Thread des Absenders über einen Rückrufmechanismus über die E / A benachrichtigen. So funktioniert das AIO- Modul in glibc. Hier sind einige vage Details zur Implementierung.
Während dies eine gute Lösung ist, die ziemlich portabel ist (solange Sie Threads haben), kann das Betriebssystem in der Regel nicht blockierende E / A effizienter warten. Dieser Wikipedia-Artikel listet mögliche Implementierungen neben dem Thread-Pool auf.
quelle
Ich bin derzeit dabei, Async io auf einer eingebetteten Plattform mithilfe von Protothreads zu implementieren. Das nicht blockierende io macht den Unterschied zwischen 16000 fps und 160 fps. Der größte Vorteil von nicht blockierendem Io ist, dass Sie Ihren Code so strukturieren können, dass er andere Aufgaben ausführt, während die Hardware ihre Aufgabe erfüllt. Auch die Initialisierung von Geräten kann parallel erfolgen.
Martin
quelle
In Node werden mehrere Threads gestartet, dies ist jedoch eine Schicht in der C ++ - Laufzeit.
https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea
https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98
Die Erklärung "Knoten ist schneller, weil er nicht blockiert ..." ist ein bisschen Marketing und das ist eine großartige Frage. Es ist effizient und skalierbar, aber nicht genau Single-Threaded.
quelle
Soweit ich weiß, besteht die Verbesserung darin, dass asynchrone E / A die sogenannten E / A-Abschlussports verwendet (ich spreche von MS System, nur um dies zu verdeutlichen) . Durch die Verwendung des asynchronen Aufrufs nutzt das Framework diese Architektur automatisch, und dies soll wesentlich effizienter sein als der Standard-Threading-Mechanismus. Als persönliche Erfahrung kann ich sagen, dass Sie Ihre Anwendung vernünftigerweise reaktiver fühlen würden, wenn Sie AsyncCalls bevorzugen, anstatt Threads zu blockieren.
quelle
Lassen Sie mich ein Gegenbeispiel geben, dass asynchrone E / A nicht funktionieren. Ich schreibe einen Proxy ähnlich dem unten verwendeten Boost :: Asio. https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp
Das Szenario in meinem Fall ist jedoch, dass eingehende (von der Clientseite) Nachrichten schnell sind, während ausgehende (zur Serverseite) für eine Sitzung langsam sind, um mit der eingehenden Geschwindigkeit Schritt zu halten oder den gesamten Proxy-Durchsatz zu maximieren, den wir verwenden müssen mehrere Sitzungen unter einer Verbindung.
Somit funktioniert dieses asynchrone E / A-Framework nicht mehr. Wir benötigen einen Thread-Pool, um ihn an den Server zu senden, indem wir jedem Thread eine Sitzung zuweisen.
quelle