Immer und immer wieder, ich sehe es so , dass mit async
- await
schafft keine zusätzlichen Threads. Das ist nicht sinnvoll, da ein Computer nur dann mehr als eine Aufgabe gleichzeitig ausführen kann
- Tatsächlich mehr als eine Sache gleichzeitig ausführen (parallel ausführen, mehrere Prozessoren verwenden)
- Simulieren Sie es, indem Sie Aufgaben planen und zwischen ihnen wechseln (machen Sie ein bisschen A, ein bisschen B, ein bisschen A usw.)
Wenn async
- await
beides nicht, wie kann dann eine Anwendung darauf reagieren? Wenn nur ein Thread vorhanden ist, bedeutet das Aufrufen einer Methode, dass Sie warten müssen, bis die Methode abgeschlossen ist, bevor Sie etwas anderes tun. Die Methoden in dieser Methode müssen auf das Ergebnis warten, bevor Sie fortfahren, und so weiter.
c#
.net
multithreading
asynchronous
async-await
Frau Corlib
quelle
quelle
await
/async
funktioniert, ohne Threads zu erstellen.Antworten:
Eigentlich ist Async / Warten nicht so magisch. Das vollständige Thema ist ziemlich weit gefasst, aber für eine schnelle und dennoch vollständige Antwort auf Ihre Frage denke ich, dass wir es schaffen können.
Lassen Sie uns ein einfaches Ereignis zum Klicken auf eine Schaltfläche in einer Windows Forms-Anwendung angehen:
Ich werde explizit nicht darüber sprechen, was es
GetSomethingAsync
gerade zurückgibt. Sagen wir einfach, dies ist etwas, das nach beispielsweise 2 Sekunden abgeschlossen sein wird.In einer traditionellen, nicht asynchronen Welt würde Ihr Ereignishandler für Schaltflächenklicks ungefähr so aussehen:
Wenn Sie auf die Schaltfläche im Formular klicken, scheint die Anwendung etwa 2 Sekunden lang einzufrieren, während wir auf den Abschluss dieser Methode warten. Was passiert ist, dass die "Nachrichtenpumpe", im Grunde eine Schleife, blockiert ist.
Diese Schleife fragt Windows ständig: "Hat jemand etwas getan, wie die Maus bewegt, auf etwas geklickt? Muss ich etwas neu streichen? Wenn ja, sagen Sie es mir!" und verarbeitet dann das "etwas". Diese Schleife erhielt eine Nachricht, dass der Benutzer auf "button1" (oder den entsprechenden Nachrichtentyp von Windows) geklickt hat, und rief schließlich unsere an
button1_Click
oben beschriebene Methode aufgerufen hat. Bis diese Methode zurückkehrt, wartet diese Schleife nicht mehr. Dies dauert 2 Sekunden und währenddessen werden keine Nachrichten verarbeitet.Die meisten Dinge, die sich mit Fenstern befassen, werden mithilfe von Nachrichten ausgeführt. Wenn die Nachrichtenschleife auch nur für eine Sekunde keine Nachrichten mehr pumpt, fällt dies dem Benutzer schnell auf. Wenn Sie beispielsweise den Notizblock oder ein anderes Programm über Ihr eigenes Programm bewegen und dann wieder entfernen, wird eine Reihe von Malmeldungen an Ihr Programm gesendet, die angeben, welcher Bereich des Fensters jetzt plötzlich wieder sichtbar wurde. Wenn die Nachrichtenschleife, die diese Nachrichten verarbeitet, auf etwas wartet, das blockiert ist, wird kein Malvorgang ausgeführt.
Wenn im ersten Beispiel
async/await
keine neuen Threads erstellt werden, wie funktioniert das?Nun, was passiert ist, dass Ihre Methode in zwei Teile geteilt ist. Dies ist eine dieser allgemeinen Themen, daher werde ich nicht zu sehr ins Detail gehen, aber es reicht zu sagen, dass die Methode in diese beiden Dinge unterteilt ist:
await
, einschließlich des Aufrufs vonGetSomethingAsync
await
Illustration:
Neu arrangiert:
Grundsätzlich läuft die Methode folgendermaßen ab:
await
Es ruft die
GetSomethingAsync
Methode auf, die ihre Sache macht, und gibt etwas zurück, das in Zukunft 2 Sekunden dauern wirdBisher befinden wir uns noch im ursprünglichen Aufruf von button1_Click, der im Hauptthread stattfindet und von der Nachrichtenschleife aufgerufen wird. Wenn der Code im Vorfeld
await
viel Zeit in Anspruch nimmt, friert die Benutzeroberfläche immer noch ein. In unserem Beispiel nicht so sehrWas das
await
Schlüsselwort zusammen mit etwas cleverer Compiler-Magie bewirkt, ist, dass es im Grunde so etwas wie "Ok, weißt du was ? Ich werde einfach vom Button-Click-Event-Handler hier zurückkehren. Wenn du (wie in, das, was wir ' Warten Sie auf), um fertig zu werden. Lassen Sie es mich wissen, da ich noch Code zum Ausführen habe. "Tatsächlich wird die SynchronizationContext-Klasse darüber informiert, dass dies abgeschlossen ist. Abhängig vom aktuellen Synchronisierungskontext, der gerade im Spiel ist, wird die Warteschlange für die Ausführung anstehen. Die in einem Windows Forms-Programm verwendete Kontextklasse stellt sie mithilfe der Warteschlange in die Warteschlange, die die Nachrichtenschleife pumpt.
Es kehrt also zur Nachrichtenschleife zurück, die nun frei ist, Nachrichten weiter zu pumpen, z. B. das Fenster zu verschieben, die Größe zu ändern oder auf andere Schaltflächen zu klicken.
Für den Benutzer reagiert die Benutzeroberfläche jetzt wieder, verarbeitet andere Schaltflächenklicks, ändert die Größe und vor allem zeichnet sie neu , sodass sie nicht einzufrieren scheint.
await
und den Rest der Methode weiter ausführen. Beachten Sie, dass dieser Code erneut aus der Nachrichtenschleife aufgerufen wird. Wenn dieser Code also etwas Langes tut, ohne ihnasync/await
ordnungsgemäß zu verwenden, blockiert er die Nachrichtenschleife erneutEs gibt hier viele bewegliche Teile unter der Haube, daher hier einige Links zu weiteren Informationen. Ich wollte sagen, "falls Sie es brauchen", aber dieses Thema ist ziemlich weit gefasst und es ist ziemlich wichtig, einige dieser beweglichen Teile zu kennen . Sie werden immer verstehen, dass Async / Warten immer noch ein undichtes Konzept ist. Einige der zugrunde liegenden Einschränkungen und Probleme fließen immer noch in den umgebenden Code ein. Wenn dies nicht der Fall ist, müssen Sie normalerweise eine Anwendung debuggen, die ohne ersichtlichen Grund zufällig unterbrochen wird.
OK, was ist, wenn
GetSomethingAsync
ein Thread hochgefahren wird, der in 2 Sekunden abgeschlossen ist? Ja, dann ist offensichtlich ein neuer Thread im Spiel. Dieser Thread ist jedoch nicht , weil die Async-heit dieses Verfahrens ist es, weil der Programmierer dieses Verfahrens ein Gewinde wählt asynchronen Code zu implementieren. Fast alle asynchronen E / A verwenden keinen Thread, sondern unterschiedliche Dinge.async/await
selbst drehen keine neuen Threads hoch, aber offensichtlich können die "Dinge, auf die wir warten" mithilfe von Threads implementiert werden.Es gibt viele Dinge in .NET, die nicht unbedingt einen eigenen Thread starten, aber dennoch asynchron sind:
SomethingSomethingAsync
oderBeginSomething
und hatEndSomething
und eineIAsyncResult
beteiligt ist.Normalerweise verwenden diese Dinge keinen Faden unter der Haube.
OK, möchten Sie etwas von diesem "breiten Thema"?
Fragen wir Try Roslyn nach unserem Button-Klick:
Versuchen Sie es mit Roslyn
Ich werde hier nicht in der vollständig generierten Klasse verlinken, aber es ist ziemlich blutiges Zeug.
quelle
await
Kann natürlich auch so verwendet werden, wie Sie es beschreiben, ist es aber im Allgemeinen nicht. Es werden nur die Rückrufe geplant (im Thread-Pool) - zwischen dem Rückruf und der Anforderung wird kein Thread benötigt.Ich erkläre es vollständig in meinem Blog-Beitrag Es gibt keinen Thread .
Zusammenfassend lässt sich sagen, dass moderne E / A-Systeme DMA (Direct Memory Access) stark nutzen. Es gibt spezielle dedizierte Prozessoren für Netzwerkkarten, Grafikkarten, Festplattencontroller, serielle / parallele Anschlüsse usw. Diese Prozessoren haben direkten Zugriff auf den Speicherbus und übernehmen das Lesen / Schreiben völlig unabhängig von der CPU. Die CPU muss das Gerät nur über den Speicherort im Speicher informieren, der die Daten enthält, und kann dann ihre eigene Aktion ausführen, bis das Gerät einen Interrupt auslöst, der die CPU darüber informiert, dass das Lesen / Schreiben abgeschlossen ist.
Sobald der Vorgang ausgeführt wird, muss die CPU keine Arbeit mehr ausführen und somit kein Thread mehr.
quelle
Task.Run
ist am besten für CPU-gebundene Aktionen geeignet , hat aber auch eine Handvoll anderer Verwendungszwecke.Es ist nicht so, dass keiner von ihnen auf etwas wartet . Denken Sie daran, dass der Zweck von
await
nicht darin besteht , synchronen Code magisch asynchron zu machen . Dies soll die Verwendung der gleichen Techniken ermöglichen , die wir zum Schreiben von synchronem Code beim Aufrufen von asynchronem Code verwenden . Beim Warten geht es darum, dass der Code, der Operationen mit hoher Latenz verwendet, wie Code aussieht, der Operationen mit niedriger Latenz verwendet . Diese Operationen mit hoher Latenz können sich auf Threads befinden, sie können sich auf Spezialhardware befinden, sie können ihre Arbeit in kleine Teile zerreißen und sie in die Nachrichtenwarteschlange stellen, um sie später vom UI-Thread zu verarbeiten. Sie tun etwas , um Asynchronität zu erreichen, aber siesind diejenigen, die es tun. Mit Await können Sie diese Asynchronität nur nutzen.Ich denke auch, dass Ihnen eine dritte Option fehlt. Wir alten Leute - Kinder von heute mit ihrer Rap-Musik sollten von meinem Rasen verschwinden usw. - erinnern uns an die Welt von Windows in den frühen neunziger Jahren. Es gab keine Multi-CPU-Maschinen und keine Thread-Scheduler. Sie wollten zwei Windows-Apps gleichzeitig ausführen, Sie mussten nachgeben . Multitasking war kooperativ . Das Betriebssystem teilt einem Prozess mit, dass er ausgeführt werden soll, und wenn er sich schlecht verhält, werden alle anderen Prozesse nicht bedient. Es läuft, bis es nachgibt, und irgendwie muss es wissen, wie es dort weitermachen soll, wo es wenn das Betriebssystem das nächste Mal die Kontrolle zurückgibt. Asynchroner Code mit einem Thread ist sehr ähnlich, mit "Warten" anstelle von "Ertrag". Warten bedeutet: "Ich werde mich daran erinnern, wo ich hier aufgehört habe, und jemand anderen eine Weile laufen lassen. Rufen Sie mich zurück, wenn die Aufgabe, auf die ich warte, abgeschlossen ist, und ich werde dort weitermachen, wo ich aufgehört habe." Ich denke, Sie können sehen, wie Apps dadurch reaktionsfähiger werden, genau wie in den Windows 3-Tagen.
Es gibt den Schlüssel, den Sie vermissen. Eine Methode kann zurückkehren, bevor ihre Arbeit abgeschlossen ist . Das ist genau dort die Essenz der Asynchronität. Eine Methode gibt eine Aufgabe zurück, die bedeutet, dass "diese Arbeit ausgeführt wird; sagen Sie mir, was zu tun ist, wenn sie abgeschlossen ist". Die Arbeit der Methode ist nicht erledigt, obwohl sie zurückgekehrt ist .
Vor dem Operator "Warten" mussten Sie Code schreiben, der aussah wie Spaghetti, die durch Schweizer Käse gefädelt wurden, um die Tatsache zu bewältigen, dass wir nach Abschluss noch etwas zu tun haben , aber die Rückgabe und die Fertigstellung desynchronisiert sind . Mit Await können Sie Code schreiben, der so aussieht, als ob die Rückgabe und der Abschluss synchronisiert sind, ohne dass sie tatsächlich synchronisiert werden.
quelle
yield
Schlüsselwort. Sowohlasync
Methoden als auch Iteratoren in C # sind eine Form der Coroutine. Dies ist der allgemeine Begriff für eine Funktion, die weiß, wie ihre aktuelle Operation für eine spätere Wiederaufnahme angehalten werden kann. Eine Reihe von Sprachen haben heutzutage Coroutinen oder koroutinenähnliche Kontrollflüsse.Ich bin wirklich froh, dass jemand diese Frage gestellt hat, denn ich habe lange Zeit auch geglaubt, dass Threads für die Parallelität notwendig sind. Als ich zum ersten Mal Event-Loops sah , dachte ich, sie wären eine Lüge. Ich dachte mir: "Es gibt keine Möglichkeit, dass dieser Code gleichzeitig ausgeführt wird, wenn er in einem einzelnen Thread ausgeführt wird." Denken Sie daran, dies ist, nachdem ich bereits den Kampf durchgemacht hatte, den Unterschied zwischen Parallelität und Parallelität zu verstehen.
Nach eigenen Recherchen habe ich endlich das fehlende Stück gefunden :
select()
. Insbesondere IO Multiplexing, implementiert durch verschiedene Kerne unter verschiedenen Namen:select()
,poll()
,epoll()
,kqueue()
. Hierbei handelt es sich um Systemaufrufe , bei denen Sie, obwohl sich die Implementierungsdetails unterscheiden, eine Reihe von Dateideskriptoren zur Überwachung übergeben können. Anschließend können Sie einen weiteren Aufruf tätigen, der blockiert, bis sich einer der überwachten Dateideskriptoren ändert.Auf diese Weise kann man auf eine Reihe von E / A-Ereignissen (die Hauptereignisschleife) warten, das erste abgeschlossene Ereignis verarbeiten und dann die Steuerung an die Ereignisschleife zurückgeben. Spülen und wiederholen.
Wie funktioniert das? Nun, die kurze Antwort ist, dass es sich um Magie auf Kernel- und Hardware-Ebene handelt. Neben der CPU befinden sich viele Komponenten in einem Computer, und diese Komponenten können parallel arbeiten. Der Kernel kann diese Geräte steuern und direkt mit ihnen kommunizieren, um bestimmte Signale zu empfangen.
Diese IO-Multiplexing-Systemaufrufe sind der grundlegende Baustein für Single-Threaded-Ereignisschleifen wie node.js oder Tornado. Wenn Sie
await
eine Funktion ausführen, suchen Sie nach einem bestimmten Ereignis (dem Abschluss dieser Funktion) und geben dann die Kontrolle an die Hauptereignisschleife zurück. Wenn das Ereignis, das Sie gerade ansehen, abgeschlossen ist, wird die Funktion (eventuell) dort fortgesetzt, wo sie aufgehört hat. Funktionen, mit denen Sie die Berechnung wie folgt anhalten und fortsetzen können, werden als Coroutinen bezeichnet .quelle
await
undasync
verwenden Sie Aufgaben nicht Threads.Das Framework verfügt über einen Thread-Pool, der bereit ist, einige Arbeiten in Form von Task- Objekten auszuführen . Wenn Sie eine Aufgabe an den Pool senden, wählen Sie einen freien, bereits vorhandenen 1- Thread aus, um die Aufgabenaktionsmethode aufzurufen.
Beim Erstellen einer Aufgabe geht es darum, ein neues Objekt zu erstellen, viel schneller als beim Erstellen eines neuen Threads.
Wenn eine Aufgabe eine Fortsetzung anhängen kann, handelt es sich um ein neues Aufgabenobjekt , das ausgeführt wird, sobald der Thread endet.
Seit der
async/await
Verwendung von Aufgaben erstellen sie keinen neuen Thread.Während Interrupt-Programmiertechniken in jedem modernen Betriebssystem weit verbreitet sind, halte ich sie hier nicht für relevant.
Sie können zwei CPU-gebundene Tasks gleichzeitig (tatsächlich verschachtelt) in einer einzelnen CPU ausführen lassen
aysnc/await
. Dieskonnte nicht einfach damit erklärt werden, dass das Betriebssystem die Warteschlange für IORP unterstützt .
Als ich das letzte Mal die vom Compiler transformierten
async
Methoden in DFA überprüft habe , ist die Arbeit in Schritte unterteilt, die jeweils mit einerawait
Anweisung enden .Der
await
startet seine Aufgabe und fügt ihm eine Fortsetzung hinzu, um den nächsten Schritt auszuführen.Als Konzeptbeispiel hier ein Pseudocode-Beispiel.
Die Dinge werden aus Gründen der Klarheit und weil ich mich nicht an alle Details genau erinnere, vereinfacht.
Es wird in so etwas verwandelt
1 Tatsächlich kann ein Pool seine Richtlinie zur Aufgabenerstellung haben.
quelle
Ich werde nicht mit Eric Lippert oder Lasse V. Karlsen und anderen konkurrieren, ich möchte nur auf eine andere Facette dieser Frage aufmerksam machen, die meiner Meinung nach nicht ausdrücklich erwähnt wurde.
Wenn Sie
await
es alleine verwenden, reagiert Ihre App nicht auf magische Weise. Wenn Sie in der Methode, auf die Sie warten, aus den UI-Thread-Blöcken etwas tun, wird Ihre UI trotzdem genauso blockiert wie die nicht erwartete Version .Sie müssen Ihre erwartete Methode speziell schreiben, damit sie entweder einen neuen Thread erzeugt oder einen Abschlussport verwendet (der die Ausführung im aktuellen Thread zurückgibt und etwas anderes zur Fortsetzung aufruft, wenn der Abschlussport signalisiert wird). Aber dieser Teil wird in anderen Antworten gut erklärt.
quelle
So sehe ich das alles, es ist vielleicht nicht besonders technisch korrekt, aber es hilft mir zumindest :).
Grundsätzlich gibt es zwei Arten der Verarbeitung (Berechnung), die auf einer Maschine ausgeführt werden:
Wenn wir also nach dem Kompilieren einen Quellcode schreiben, ist die Verarbeitung abhängig vom verwendeten Objekt (und dies ist sehr wichtig) CPU-gebunden oder E / A-gebunden , und tatsächlich kann sie an eine Kombination von gebunden werden beide.
Einige Beispiele:
FileStream
Objekts (das ein Stream ist) verwende, wird die Verarbeitung beispielsweise mit 1% CPU-Bindung und 99% E / A-Bindung ausgeführt.NetworkStream
Objekts (das ein Stream ist) verwende, wird die Verarbeitung beispielsweise mit 1% CPU-Bindung und 99% E / A-Bindung ausgeführt.Memorystream
Objekts (das ein Stream ist) verwende, ist die Verarbeitung zu 100% CPU-gebunden.Wie Sie sehen, aus objektorientierter Sicht eines Programmierers, obwohl ich immer auf a zugreife
Stream
Objekt , das, was darunter passiert, stark vom endgültigen Typ des Objekts abhängen.Um die Dinge zu optimieren, ist es manchmal nützlich, Code parallel ausführen zu können (beachten Sie, dass ich das Wort nicht asynchron verwende), wenn dies möglich und / oder erforderlich ist.
Einige Beispiele:
Vor async / await hatten wir im Wesentlichen zwei Lösungen dafür:
Das asynchrone / Warten ist nur ein allgemeines Programmiermodell, das auf dem Task-Konzept basiert . Es ist etwas einfacher zu verwenden als Threads oder Thread-Pools für CPU-gebundene Aufgaben und viel einfacher zu verwenden als das alte Begin / End-Modell. Undercovers ist jedoch "nur" ein hochentwickelter Wrapper mit allen Funktionen.
So ist der eigentliche Gewinn meist auf IO Bound Aufgaben , Aufgabe, die die CPU nicht verwenden, aber async / await ist nach wie vor nur ein Programmiermodell, ist es nicht Sie zu bestimmen , hilft wie / wo Verarbeitung am Ende passieren wird.
Dies bedeutet nicht, dass eine Klasse eine Methode "DoSomethingAsync" hat, die ein Task-Objekt zurückgibt, von dem Sie annehmen können, dass es CPU-gebunden ist (was bedeutet, dass es möglicherweise ziemlich nutzlos ist , insbesondere wenn es keinen Abbruch-Token-Parameter hat) oder IO-gebunden (was bedeutet, dass es wahrscheinlich ein Muss ist ) oder eine Kombination aus beiden (da das Modell ziemlich viral ist, können Bindungen und potenzielle Vorteile am Ende sehr gemischt und nicht so offensichtlich sein).
Zurück zu meinen Beispielen: Wenn ich meine Schreibvorgänge mit async / await in MemoryStream ausführe, bleibt die CPU gebunden (ich werde wahrscheinlich nicht davon profitieren), obwohl ich sicherlich von Dateien und Netzwerkströmen profitieren werde.
quelle
Weitere Antworten zusammenfassen:
Async / await wird hauptsächlich für E / A-gebundene Aufgaben erstellt, da durch deren Verwendung vermieden werden kann, dass der aufrufende Thread blockiert wird. Ihre Hauptverwendung liegt bei UI-Threads, bei denen es nicht erwünscht ist, dass der Thread bei einer E / A-gebundenen Operation blockiert wird.
Async erstellt keinen eigenen Thread. Der Thread der aufrufenden Methode wird verwendet, um die asynchrone Methode auszuführen, bis eine erwartete gefunden wird. Der gleiche Thread führt dann den Rest der aufrufenden Methode über den asynchronen Methodenaufruf hinaus weiter aus. Innerhalb der aufgerufenen asynchronen Methode kann die Fortsetzung nach der Rückkehr vom Warten auf einen Thread aus dem Thread-Pool ausgeführt werden - der einzige Ort, an dem ein separater Thread ins Bild kommt.
quelle
Ich versuche es von unten nach oben zu erklären. Vielleicht findet es jemand hilfreich. Ich war dort, habe das getan, es neu erfunden, als ich einfache Spiele unter DOS in Pascal gemacht habe (gute alte Zeiten ...)
Also ... In jeder ereignisgesteuerten Anwendung befindet sich eine Ereignisschleife, die ungefähr so aussieht:
Frameworks verbergen dieses Detail normalerweise vor Ihnen, aber es ist da. Die Funktion getMessage liest das nächste Ereignis aus der Ereigniswarteschlange oder wartet, bis ein Ereignis eintritt: Maus bewegen, Tastendruck, Tastendruck, Klicken usw. Anschließend sendet dispatchMessage das Ereignis an den entsprechenden Ereignishandler. Wartet dann auf das nächste Ereignis und so weiter, bis ein Beendigungsereignis eintritt, das die Schleifen verlässt und die Anwendung beendet.
Ereignishandler sollten schnell ausgeführt werden, damit die Ereignisschleife mehr Ereignisse abfragen kann und die Benutzeroberfläche weiterhin reagiert. Was passiert, wenn ein Knopfdruck einen teuren Vorgang wie diesen auslöst?
Nun, die Benutzeroberfläche reagiert nicht mehr, bis der 10-Sekunden-Vorgang abgeschlossen ist, da die Steuerung innerhalb der Funktion bleibt. Um dieses Problem zu lösen, müssen Sie die Aufgabe in kleine Teile aufteilen, die schnell ausgeführt werden können. Dies bedeutet, dass Sie nicht das Ganze in einem einzigen Ereignis behandeln können. Sie müssen einen kleinen Teil der Arbeit erledigen und dann ein anderes Ereignis veröffentlichen in die Ereigniswarteschlange stellen, um die Fortsetzung anzufordern.
Sie würden dies also ändern in:
In diesem Fall wird nur die erste Iteration ausgeführt, dann wird eine Nachricht an die Ereigniswarteschlange gesendet, um die nächste Iteration auszuführen, und es wird zurückgegeben. Es ist unser Beispiel
postFunctionCallMessage
Pseudofunktion wird ein Ereignis "Diese Funktion aufrufen" in die Warteschlange gestellt, sodass der Ereignis-Dispatcher es aufruft, wenn es es erreicht. Auf diese Weise können alle anderen GUI-Ereignisse verarbeitet werden, während auch Teile einer lang laufenden Arbeit kontinuierlich ausgeführt werden.Solange diese lange laufende Aufgabe ausgeführt wird, befindet sich ihr Fortsetzungsereignis immer in der Ereigniswarteschlange. Sie haben also im Grunde Ihren eigenen Taskplaner erfunden. Wobei die Fortsetzungsereignisse in der Warteschlange "Prozesse" sind, die ausgeführt werden. Genau das tun Betriebssysteme, außer dass das Senden der Fortsetzungsereignisse und das Zurückkehren zur Scheduler-Schleife über den Timer-Interrupt der CPU erfolgt, bei dem das Betriebssystem den Kontextumschaltcode registriert hat, sodass Sie sich nicht darum kümmern müssen. Aber hier schreiben Sie Ihren eigenen Planer, also müssen Sie sich darum kümmern - bis jetzt.
So können wir Aufgaben mit langer Laufzeit in einem einzigen Thread parallel zur GUI ausführen, indem wir sie in kleine Teile aufteilen und Fortsetzungsereignisse senden. Dies ist die allgemeine Idee der
Task
Klasse. Es stellt ein Stück Arbeit dar und wenn Sie es aufrufen.ContinueWith
, definieren Sie, welche Funktion als nächstes Stück aufgerufen werden soll, wenn das aktuelle Stück fertig ist (und sein Rückgabewert an die Fortsetzung übergeben wird). DieTask
Klasse verwendet einen Thread-Pool, in dem sich in jedem Thread eine Ereignisschleife befindet, die darauf wartet, ähnliche Arbeiten auszuführen, wie ich sie zu Beginn gezeigt habe. Auf diese Weise können Millionen von Aufgaben parallel ausgeführt werden, aber nur wenige Threads, um sie auszuführen. Aber es würde genauso gut mit einem einzelnen Thread funktionieren - solange Ihre Aufgaben ordnungsgemäß in kleine Teile aufgeteilt sind, scheint jeder von ihnen parallel zu laufen.Aber all diese Verkettungen manuell in kleine Teile aufzuteilen, ist eine mühsame Arbeit und bringt das Layout der Logik völlig durcheinander, da der gesamte Hintergrundaufgabencode im Grunde genommen ein
.ContinueWith
Chaos ist. Hier hilft Ihnen der Compiler. Es erledigt all diese Verkettung und Fortsetzung für Sie im Hintergrund. Wenn Sie sagen, dassawait
Sie dem Compiler mitteilen, dass "hier anhalten, den Rest der Funktion als Fortsetzungsaufgabe hinzufügen". Der Compiler kümmert sich um den Rest, so dass Sie nicht müssen.quelle
Tatsächlich sind
async await
Ketten Zustandsmaschinen, die vom CLR-Compiler generiert werden.async await
Verwendet jedoch Threads, die TPL zum Ausführen von Aufgaben im Thread-Pool verwendet.Der Grund, warum die Anwendung nicht blockiert wird, besteht darin, dass die Zustandsmaschine entscheiden kann, welche Co-Routine ausgeführt, wiederholt, überprüft und erneut entschieden werden soll.
Weiterführende Literatur:
Was generiert async & await?
Async Await und die generierte StateMachine
Asynchrones C # und F # (III.): Wie funktioniert es? - Tomas Petricek
Bearbeiten :
In Ordnung. Es scheint, als ob meine Ausarbeitung falsch ist. Ich muss jedoch darauf hinweisen, dass Zustandsautomaten wichtige Vermögenswerte für
async await
s sind. Selbst wenn Sie asynchrone E / A aufnehmen, benötigen Sie einen Helfer, um zu überprüfen, ob der Vorgang abgeschlossen ist. Daher benötigen wir weiterhin eine Zustandsmaschine und bestimmen, welche Routine zusammen asychron ausgeführt werden kann.quelle
Dies beantwortet die Frage nicht direkt, aber ich denke, es ist eine interessante zusätzliche Information:
Async and await erstellt selbst keine neuen Threads. ABER je nachdem, wo Sie asynchrones Warten verwenden, wird der synchrone Teil VOR dem Warten möglicherweise auf einem anderen Thread ausgeführt als der synchrone Teil NACH dem Warten (z. B. verhalten sich ASP.NET und ASP.NET-Kern unterschiedlich).
In UI-Thread-basierten Anwendungen (WinForms, WPF) befinden Sie sich vorher und nachher im selben Thread. Wenn Sie jedoch async away für einen Thread-Pool-Thread verwenden, ist der Thread vor und nach dem Warten möglicherweise nicht identisch.
Ein tolles Video zu diesem Thema
quelle