Hallo an alle!
Heute finden Sie einen langen Text ohne Bilder (im Vergleich zum Original leicht gekĂŒrzt), in dem die in der Ăberschrift dargelegte These detailliert analysiert wird. Microsoft-Veteran
Terry Crowley beschreibt die Essenz der asynchronen Programmierung und erklÀrt, warum dieser Ansatz viel realistischer und angemessener ist als synchron und sequentiell.
Wer ein Buch schreiben möchte oder denkt, das solche Themen berĂŒhrt, schreibt persönlich.
SynchronizitÀt ist ein Mythos. Nichts passiert sofort. Alles braucht Zeit.
Einige Eigenschaften von Computersystemen und Programmierumgebungen beruhen im Wesentlichen auf der Tatsache, dass Berechnungen in der dreidimensionalen physikalischen Welt stattfinden und durch die Grenzen der Lichtgeschwindigkeit und die Gesetze der Thermodynamik begrenzt sind.
Eine solche Verwurzelung in der physischen Welt bedeutet, dass einige Aspekte auch mit dem Aufkommen neuer Technologien, die neue Möglichkeiten und Zugang zu neuen Grenzen der ProduktivitĂ€t bieten, nicht an Relevanz verlieren. Sie bleiben gĂŒltig, weil sie nicht nur âwĂ€hrend des Entwurfs gewĂ€hlte Optionenâ sind, sondern die zugrunde liegende RealitĂ€t des physischen Universums.
Der Unterschied zwischen SynchronitĂ€t und AsynchronitĂ€t in der Sprache und der Schaffung von Systemen ist genau ein Aspekt des Designs, der tiefe physikalische Grundlagen hat. Die meisten Programmierer beginnen sofort mit solchen Programmen und Sprachen zu arbeiten, bei denen eine synchrone AusfĂŒhrung impliziert ist. TatsĂ€chlich ist dies so natĂŒrlich, dass niemand direkt darĂŒber spricht oder spricht. Der Begriff âsynchronâ bedeutet in diesem Zusammenhang, dass die Berechnung wie eine Reihe aufeinanderfolgender Schritte sofort erfolgt und nichts anderes passiert, bevor sie abgeschlossen ist. Ich fĂŒhre
âc = a + bâ âx = f(y)â
- und nichts anderes wird passieren, bis diese Anweisung abgeschlossen ist.
NatĂŒrlich passiert im physischen Universum nichts sofort. Alle Prozesse sind mit einigen Verzögerungen verbunden. Sie mĂŒssen in der Speicherhierarchie navigieren, einen Prozessorzyklus ausfĂŒhren, Informationen von einem Festplattenlaufwerk lesen oder ĂŒber das Netzwerk eine Verbindung zu einem anderen Knoten herstellen, was ebenfalls zu Verzögerungen bei der DatenĂŒbertragung fĂŒhrt. All dies ist eine grundlegende Folge der Lichtgeschwindigkeit und der Signalausbreitung in drei Dimensionen.
Alle Prozesse sind etwas spÀt, alles braucht Zeit. Wenn wir einige Prozesse als synchron definieren, sagen wir im Wesentlichen, dass wir diese Verzögerung ignorieren und unsere Berechnung als augenblicklich beschreiben werden. TatsÀchlich wird in Computersystemen hÀufig eine seriöse Infrastruktur eingerichtet, die es Ihnen ermöglicht, die grundlegende Hardware weiterhin aktiv zu nutzen, selbst wenn versucht wird, die Programmierschnittstelle zu optimieren, indem die darauf auftretenden Ereignisse als synchron dargestellt werden.
Die bloĂe Vorstellung, dass die Synchronisation ĂŒber einen speziellen Mechanismus erfolgt und mit Kosten verbunden ist, mag fĂŒr den Programmierer unlogisch erscheinen, der eher daran gewöhnt ist, dass AsynchronitĂ€t eine aktive externe Steuerung erfordert. TatsĂ€chlich passiert dies tatsĂ€chlich, wenn eine asynchrone Schnittstelle bereitgestellt wird: Eine echte grundlegende AsynchronitĂ€t öffnet sich fĂŒr den Programmierer etwas deutlicher als zuvor, und er muss sie manuell verarbeiten, anstatt sich auf ein Programm zu verlassen, das dies automatisch tun könnte. Die direkte Bereitstellung von AsynchronitĂ€t ist mit zusĂ€tzlichen Kosten fĂŒr den Programmierer verbunden, ermöglicht es Ihnen jedoch gleichzeitig, die in diesem Themenbereich enthaltenen Kosten und Kompromisse kompetenter zu verteilen, und ĂŒberlĂ€sst dies nicht einem System, das solche Kosten und Kompromisse ausgleichen wĂŒrde. Die asynchrone Schnittstelle entspricht hĂ€ufig genauer Ereignissen, die physikalisch im Basissystem auftreten, und eröffnet dementsprechend zusĂ€tzliche Optimierungsmöglichkeiten.
Beispielsweise erhalten der Prozessor und das Speichersystem eine angemessene Infrastruktur, die fĂŒr das Lesen und Schreiben von Daten im Speicher unter BerĂŒcksichtigung ihrer Hierarchie verantwortlich ist. Auf Ebene 1 (L1) kann die Cache-Verbindung mehrere Nanosekunden dauern, wĂ€hrend die Speicherverbindung selbst den gesamten Weg durch L2, L3 und den Hauptspeicher durchlaufen muss, was Hunderte von Nanosekunden dauern kann. Wenn Sie nur warten, bis die Speicherverbindung aufgelöst ist, ist der Prozessor fĂŒr einen erheblichen Prozentsatz der Zeit inaktiv.
Zur Optimierung solcher PhĂ€nomene werden schwerwiegende Mechanismen verwendet: Pipelining mit einer fĂŒhrenden Ansicht des Befehlsstroms, gleichzeitiges Abrufen mehrerer VorgĂ€nge aus dem Speicher und der aktuellen Datenspeicherung, Verzweigungsvorhersage und Versuche, das Programm weiter zu optimieren, selbst wenn es zu einem anderen Speicherort springt, genaue Kontrolle der Speicherbarrieren, um dies zu gewĂ€hrleisten dass all dieser komplexe Mechanismus weiterhin ein konsistentes Speichermodell fĂŒr eine ĂŒbergeordnete Programmierumgebung bereitstellen wird. All diese Dinge werden unternommen, um die Leistung zu optimieren und die Hardware maximal zu nutzen, um diese Verzögerungen von 10 bis 100 Nanosekunden in der Speicherhierarchie zu verbergen und ein System bereitzustellen, in dem eine synchrone AusfĂŒhrung stattfinden soll, wĂ€hrend gleichzeitig die Leistung des Prozessorkerns beeintrĂ€chtigt wird.
Es ist keineswegs immer klar, wie effektiv solche Optimierungen fĂŒr einen bestimmten Code sind, und zur Beantwortung dieser Frage sind hĂ€ufig sehr spezifische Tools zur Leistungsanalyse erforderlich. Solche analytischen Arbeiten sind bei der Entwicklung einiger sehr wertvoller Codes vorgesehen (z. B. in der Konvertierungs-Engine fĂŒr Excel, einigen Komprimierungsoptionen im Kernel oder kryptografischen Pfaden im Code).
Operationen mit einer gröĂeren Verzögerung, beispielsweise das Lesen von Daten von einer rotierenden Platte, erfordern die Verwendung anderer Mechanismen. In solchen FĂ€llen muss beim Anfordern des Lesens von der Betriebssystemfestplatte vollstĂ€ndig zu einem anderen Thread oder Prozess gewechselt werden, und die synchrone Anforderung bleibt nicht gesendet. Die hohen Kosten fĂŒr das Umschalten und UnterstĂŒtzen dieses Mechanismus als solchen sind akzeptabel, da die in diesem Fall verborgene Latenz mehrere Millisekunden anstelle von Nanosekunden erreichen kann. Bitte beachten Sie: Diese Kosten reduzieren sich nicht auf das einfache Wechseln zwischen Threads, sondern umfassen die Kosten aller Speicher und Ressourcen, die tatsĂ€chlich inaktiv bleiben, bis der Vorgang abgeschlossen ist. All diese Kosten mĂŒssen anfallen, um eine vermeintlich synchrone Schnittstelle bereitzustellen.
Es gibt eine Reihe grundlegender GrĂŒnde, warum es erforderlich sein kann, die tatsĂ€chliche grundlegende AsynchronitĂ€t im System aufzudecken, und fĂŒr die es vorzuziehen wĂ€re, eine asynchrone Schnittstelle mit einer bestimmten Komponente, Ebene oder Anwendung zu verwenden, selbst unter BerĂŒcksichtigung der Notwendigkeit, die zunehmende KomplexitĂ€t direkt zu bewĂ€ltigen.
ParallelitĂ€t Wenn die bereitgestellte Ressource auf echte ParallelitĂ€t ausgelegt ist, kann der Client ĂŒber die asynchrone Schnittstelle auf natĂŒrliche Weise gleichzeitig mehrere Anforderungen ausgeben und verwalten, um die grundlegenden Ressourcen voll auszunutzen.
Beförderung . Der ĂŒbliche Weg, um die tatsĂ€chliche Verzögerung auf einer Schnittstelle zu verringern, besteht darin, sicherzustellen, dass mehrere Anforderungen gleichzeitig auf den Versand warten (wie viel dies fĂŒr die Leistung tatsĂ€chlich nĂŒtzlich ist, hĂ€ngt davon ab, woher wir die Quelle der Verzögerung beziehen). In jedem Fall kann, wenn das System fĂŒr das Pipelining angepasst ist, die tatsĂ€chliche Verzögerung um einen Faktor reduziert werden, der der Anzahl der Anforderungen entspricht, die auf das Senden warten. Es kann also 10 ms dauern, bis eine bestimmte Anforderung abgeschlossen ist. Wenn Sie jedoch 10 Anforderungen in die Pipeline schreiben, kann die Antwort jede Millisekunde erfolgen. Der Gesamtdurchsatz ist eine Funktion des verfĂŒgbaren Pipelining und nicht nur eine Durchlaufverzögerung pro Anforderung. Eine synchrone Schnittstelle, die eine Anforderung ausgibt und auf eine Antwort wartet, fĂŒhrt immer zu einer höheren End-to-End-Verzögerung.
Verpackung (lokal oder fern) . Die asynchrone Schnittstelle bietet auf natĂŒrlichere Weise die Implementierung eines Abfrageverpackungssystems entweder lokal oder auf einer Remote-Ressource (Hinweis: In diesem Fall kann die âFestplatteâ am anderen Ende der E / A-Schnittstelle âRemoteâ sein). Tatsache ist, dass die Anwendung den Empfang der Antwort bereits bewĂ€ltigen sollte und gleichzeitig eine gewisse Verzögerung auftritt, da die Anwendung die aktuelle Verarbeitung nicht unterbricht. Eine solche zusĂ€tzliche Verarbeitung kann mit zusĂ€tzlichen Anforderungen verbunden sein, die natĂŒrlich gebĂŒndelt wĂŒrden.
Lokale Stapelverarbeitung kann eine effizientere Ăbertragung von Anforderungsserien oder sogar die Komprimierung und Entfernung doppelter Anforderungen direkt auf dem lokalen Computer ermöglichen. Um gleichzeitig auf eine ganze Reihe von Anforderungen auf einer Remote-Ressource zugreifen zu können, ist möglicherweise eine ernsthafte Optimierung erforderlich. Ein klassisches Beispiel: Ein Plattencontroller ordnet eine Reihe von Lese- und SchreibvorgĂ€ngen neu an, um die Position des Plattenkopfs auf einer rotierenden Platte auszunutzen und die Kopfvorschubzeit zu minimieren. Auf jeder Datenspeicherschnittstelle, die auf Blockebene ausgefĂŒhrt wird, können Sie die Leistung erheblich verbessern, indem Sie eine Reihe von Abfragen bĂŒndeln, bei denen alle Lese- und SchreibvorgĂ€nge auf denselben Block fallen.
NatĂŒrlich kann lokales Packaging auch auf einer synchronen Schnittstelle implementiert werden, aber dafĂŒr mĂŒssen Sie entweder die Wahrheit weitgehend âverbergenâ oder die Paketpaketierung als Besonderheit der Schnittstelle programmieren, was den gesamten Client erheblich komplizieren kann. Ein klassisches Beispiel fĂŒr das Verstecken der Wahrheit ist gepufferte E / A. Die Anwendung ruft
âwrite(byte)â
, und die Schnittstelle gibt den
success
zurĂŒck. TatsĂ€chlich wird der Datensatz selbst (sowie Informationen darĂŒber, ob er erfolgreich ĂŒbergeben wurde) jedoch erst ausgefĂŒhrt, wenn der Puffer explizit gefĂŒllt oder leer ist. Dies geschieht, wenn die Datei geschlossen wird . Viele Anwendungen können solche Details ignorieren - ein Durcheinander tritt nur auf, wenn die Anwendung einige interagierende Abfolgen von VorgĂ€ngen sowie eine genaue Vorstellung davon, was auf den unteren Ebenen geschieht, gewĂ€hrleisten muss.
Entsperren / Entfesseln . Eine der hĂ€ufigsten Anwendungen der AsynchronitĂ€t im Kontext grafischer BenutzeroberflĂ€chen besteht darin, zu verhindern, dass der Hauptthread der BenutzeroberflĂ€che blockiert wird, damit der Benutzer weiterhin mit der Anwendung interagieren kann. Verzögerungen im Langzeitbetrieb (z. B. Netzwerkkommunikation) können nicht hinter einer synchronen Schnittstelle verborgen werden. In diesem Fall muss der BenutzeroberflĂ€chenthread solche asynchronen VorgĂ€nge explizit verwalten und die zusĂ€tzliche KomplexitĂ€t bewĂ€ltigen, die in das Programm eingefĂŒhrt wird.
Die BenutzeroberflĂ€che ist nur ein Beispiel dafĂŒr, dass die Komponente weiterhin auf zusĂ€tzliche Anforderungen reagieren muss und sich daher nicht auf einen Standardmechanismus verlassen kann, der Verzögerungen verbirgt, um die Arbeit des Programmierers zu vereinfachen.
Eine Webserverkomponente, die neue Verbindungen zu Sockets empfĂ€ngt, ĂŒbertrĂ€gt eine solche Verbindung in der Regel sehr schnell an eine andere asynchrone Komponente, die die Kommunikation auf dem Socket bereitstellt, und kehrt selbst zur Verarbeitung neuer Anforderungen zurĂŒck.
In synchronen Modellen sind Komponenten und ihre Verarbeitungsmodelle normalerweise eng miteinander verbunden.
Asynchrone Wechselwirkungen sind ein Mechanismus, der
hÀufig zur Lockerung der Bindung verwendet wird .
Kostensenkung und Management. Wie oben erwĂ€hnt, beinhaltet jeder Mechanismus zum Ausblenden der AsynchronitĂ€t eine gewisse Ressourcenzuweisung und einen gewissen Overhead. FĂŒr eine bestimmte Anwendung ist ein solcher Overhead möglicherweise nicht akzeptabel, und der Entwickler dieser Anwendung muss einen Weg finden, um die natĂŒrliche AsynchronitĂ€t zu steuern.
Ein interessantes Beispiel ist die Geschichte der Webserver. FrĂŒhe Webserver (auf Unix aufgebaut) verwendeten normalerweise einen separaten Prozess, um eingehende Anforderungen zu verwalten. Dann konnte dieser Prozess diese Verbindung lesen und darauf schreiben, es geschah im Wesentlichen synchron. Ein solches Design entwickelte sich und die Kosten wurden reduziert, als Threads anstelle von Prozessen verwendet wurden, aber das gesamte synchrone AusfĂŒhrungsmodell blieb erhalten. Bei modernen Entwurfsoptionen wird anerkannt, dass das Hauptaugenmerk nicht auf das Berechnungsmodell gelegt werden sollte, sondern vor allem auf die zugehörige Eingabe / Ausgabe in Bezug auf Lesen und Schreiben beim Austausch von Informationen mit einer Datenbank, einem Dateisystem oder der Ăbertragung von Informationen ĂŒber ein Netzwerk, wĂ€hrend eine Antwort formuliert wird . In der Regel werden hierfĂŒr Arbeitswarteschlangen verwendet, in denen eine bestimmte Begrenzung der Anzahl der Threads zulĂ€ssig ist - und in diesem Fall ist es möglich, das Ressourcenmanagement klarer zu gestalten.
Der Erfolg von NodeJS bei der Backend-Entwicklung erklĂ€rt sich nicht nur aus der UnterstĂŒtzung dieser Engine durch zahlreiche JavaScript-Entwickler, die mit der Erstellung von Client-Webschnittstellen aufgewachsen sind. In NodeJS wird wie in der Browser-Skripterstellung besonderes Augenmerk auf das asynchrone Entwerfen gelegt, was gut zu typischen Serverladeoptionen passt: Die Verwaltung der Serverressourcen hĂ€ngt in erster Linie von der E / A und nicht von der Verarbeitung ab.
Es gibt noch einen weiteren interessanten Aspekt: ââSolche Kompromisse werden vom Anwendungsentwickler expliziter und besser angepasst, wenn Sie sich an den asynchronen Ansatz halten. In dem Beispiel mit Verzögerungen in der Speicherhierarchie nahm die tatsĂ€chliche Verzögerung (gemessen in Prozessorzyklen als Anforderung im Speicher) ĂŒber mehrere Jahrzehnte dramatisch zu. Prozessorentwickler haben Schwierigkeiten, immer neue Cache-Ebenen und zusĂ€tzliche Mechanismen hinzuzufĂŒgen, die das vom Prozessor bereitgestellte Speichermodell zunehmend vorantreiben, damit das Erscheinungsbild der synchronen Verarbeitung weiter erhalten bleibt.
Die Kontextumschaltung an den Grenzen synchroner E / A ist ein weiteres Beispiel, bei dem sich die tatsĂ€chlichen Kompromisse im Laufe der Zeit dramatisch geĂ€ndert haben. Die Zunahme der Prozessorzyklen ist schneller als der Kampf gegen Verzögerungen, und dies bedeutet, dass der Anwendung jetzt viel mehr RechenfĂ€higkeiten fehlen, wĂ€hrend sie in gesperrter Form im Leerlauf ist und auf den Abschluss der E / A wartet. Das gleiche Problem im Zusammenhang mit den relativen Kosten von Kompromissen hat OS-Entwickler dazu veranlasst, sich an Speicherverwaltungsschemata zu halten, die frĂŒheren Modellen mit Prozessaustausch (bei denen das gesamte Prozessabbild vollstĂ€ndig in den Speicher geladen wird und nach dem der Prozess startet) viel Ă€hnlicher sind, anstatt sich auszutauschen Seiten. Es ist zu schwierig, Verzögerungen zu verbergen, die am Rand jeder Seite auftreten können. Der dramatisch verbesserte Gesamtdurchsatz, der mit groĂen sequentiellen E / A-Anforderungen erzielt wird (im Vergleich zur Verwendung zufĂ€lliger Anforderungen), trĂ€gt ebenfalls zu solchen Ănderungen bei.
Andere ThemenAbbrechenStornierung ist ein komplexes Thema . In der Vergangenheit haben synchron ausgerichtete Systeme bei der Stornierungsverarbeitung schlechte Arbeit geleistet, und einige unterstĂŒtzten die Stornierung ĂŒberhaupt nicht. Die Stornierung musste im Wesentlichen "out of band" ausgelegt sein, fĂŒr eine solche Operation musste ein separater AusfĂŒhrungsthread aufgerufen werden. Als Alternative eignen sich asynchrone Modelle, bei denen die UnterstĂŒtzung der Stornierung natĂŒrlicher organisiert ist, insbesondere wird ein derart trivialer Ansatz verwendet: Sie ignoriert einfach, welche Antwort letztendlich zurĂŒckkehrt (und ob sie ĂŒberhaupt zurĂŒckkehrt). Die Stornierung wird immer wichtiger, wenn die VariabilitĂ€t der Verzögerungen zunimmt, und in der Praxis nimmt auch die Fehlerrate zu - ein sehr guter historischer Ausschnitt, der zeigt, wie sich unsere Netzwerkumgebungen entwickelt haben.
Drosselung / RessourcenmanagementDas synchrone Design fĂŒhrt per Definition zu einer gewissen Drosselung, wodurch verhindert wird, dass die Anwendung zusĂ€tzliche Anforderungen ausgibt, bis die aktuelle Anforderung abgeschlossen ist. In einem asynchronen Entwurf erfolgt die Drosselung nicht umsonst, daher ist es manchmal erforderlich, sie explizit zu implementieren. In diesem
Beitrag wird die Situation mit Word Web App als Beispiel beschrieben, in der der Ăbergang vom synchronen zum asynchronen Entwurf schwerwiegende Probleme bei der Ressourcenverwaltung verursachte. Wenn die Anwendung eine synchrone Schnittstelle verwendet, erkennt sie möglicherweise nicht, dass die Drosselung implizit in den Code eingebettet ist. Wenn Sie eine solche implizite Drosselung entfernen, ist es möglich (oder erforderlich), das Ressourcenmanagement expliziter zu organisieren.
Ich musste mich gleich zu Beginn meiner Karriere damit befassen, als wir einen Texteditor von Suns synchroner grafischer API auf X Windows portierten. Bei Verwendung der Sun-API war der Rendervorgang synchron, sodass der Client die Kontrolle erst nach Abschluss wiedererlangte. In X Windows wurde eine grafische Anforderung asynchron ĂŒber eine Netzwerkverbindung gesendet und dann vom Anzeigeserver (der sich auf demselben oder einem anderen Computer befinden kann) ausgefĂŒhrt.
Um eine gute interaktive Leistung zu gewĂ€hrleisten, sollte unsere Anwendung ein gewisses Rendering bereitstellen (dh sicherstellen, dass die Zeile, in der sich der Cursor jetzt befindet, aktualisiert und gerendert wird), und dann prĂŒfen, ob andere Tastatureingaben gelesen werden mĂŒssen. , ( , ), , . API. , , - . , . UI , .
, 30 (-, Facebook iPhone ). â ( , ), , . , , .
, . , Microsoft, , API â , , . , , â : «, !» , , .
, . â , . , : , , , . , - . , ,
async/await
. «» , , , JavaScript. : , .
Async/await
, , . . , , , .
. , , . , , , . , , ( !). () , , .
, . , . async/await, , , , .
, , , â . , . â , , , ( , â Word Excel). , , - , , , .
, , , , .
, â . .
Schlussfolgerungen. â , , . , , , . , ; , .