it-swarm.com.de

Wenn async-await keine zusätzlichen Threads erstellt, wie reagiert es dann auf Anwendungen?

Immer wieder sehe ich, dass die Verwendung von async-await keine zusätzlichen Threads erzeugt. Das macht keinen Sinn, da die einzige Möglichkeit, auf der ein Computer scheinbar mehr als eine Aufgabe gleichzeitig erledigt, darin besteht,

  • Tatsächlich mehr als eine Aufgabe gleichzeitig ausführen (parallel ausführen, mehrere Prozessoren verwenden)
  • Simulieren Sie es, indem Sie Aufgaben planen und zwischen ihnen wechseln (ein bisschen A, ein bisschen B, ein bisschen A usw.)

Also, wenn async-await keines von beiden tut, wie kann es dann eine Anwendung reagieren lassen? Wenn es nur einen Thread gibt, bedeutet das Aufrufen einer Methode, dass auf den Abschluss der Methode gewartet wird, bevor etwas anderes ausgeführt wird. Die Methoden in dieser Methode müssen auf das Ergebnis warten, bevor sie fortfahren können.

209
Ms. Corlib

Eigentlich ist async/await nicht so magisch. Das gesamte Thema ist ziemlich weit gefasst, aber für eine schnelle und dennoch vollständige Beantwortung Ihrer Frage denke ich, dass wir das schaffen können.

Beheben wir ein einfaches Schaltflächenklickereignis in einer Windows Forms-Anwendung:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

Ich werde explizit ​​ nicht darüber sprechen, was immer es ist GetSomethingAsync kehrt vorerst zurück. Sagen wir einfach, dies ist etwas, das nach 2 Sekunden abgeschlossen sein wird.

In einer herkömmlichen, nicht asynchronen Welt würde die Ereignisbehandlungsroutine für das Klicken auf eine Schaltfläche ungefähr so ​​aussehen:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

Wenn Sie auf die Schaltfläche im Formular klicken, friert die Anwendung ungefähr 2 Sekunden lang ein, während wir warten, bis diese Methode abgeschlossen ist. Was passiert ist, dass die "Nachrichtenpumpe", im Grunde eine Schleife, blockiert ist.

In dieser Schleife werden Sie fortlaufend gefragt: "Hat jemand etwas getan, z. B. die Maus bewegt, auf etwas geklickt? Muss ich etwas neu streichen? Wenn ja, sagen Sie es mir!" und verarbeitet dann dieses "etwas". Diese Schleife hat eine Nachricht erhalten, dass der Benutzer auf "button1" (oder den entsprechenden Nachrichtentyp von Windows) geklickt hat und am Ende unsere obige button1_Click - 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 mit Fenstern zu tun haben, werden über Nachrichten erledigt. Das heißt, wenn die Nachrichtenschleife auch nur für eine Sekunde aufhört, Nachrichten zu pumpen, ist dies für den Benutzer schnell erkennbar. Wenn Sie beispielsweise den Editor oder ein anderes Programm über Ihr eigenes Programm verschieben und dann wieder entfernen, wird eine Reihe von Paint-Meldungen an Ihr Programm gesendet, die angeben, welcher Bereich des Fensters nun plötzlich wieder sichtbar wurde. Wenn die Nachrichtenschleife, die diese Nachrichten verarbeitet, auf etwas wartet, das blockiert ist, wird kein Bild erstellt.

Wenn also im ersten Beispiel async/await Keine neuen Threads erstellt, wie geht das?

Nun, was passiert ist, dass Ihre Methode in zwei Teile geteilt wird. Dies ist einer dieser weit gefassten Themenbereiche, damit ich nicht zu sehr ins Detail gehe, aber es genügt zu sagen, dass die Methode in diese beiden Bereiche unterteilt ist:

  1. Der gesamte Code, der zu await führt, einschließlich des Aufrufs von GetSomethingAsync
  2. Der gesamte Code nach await

Illustration:

code... code... code... await X(); ... code... code... code...

Neu angeordnet:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

Grundsätzlich läuft die Methode so ab:

  1. Es führt alles bis zu await aus
  2. Es ruft die Methode GetSomethingAsync auf, die ihre Sache macht, und gibt etwas zurück, das in Zukunft 2 Sekunden vervollständigt wird

    Bisher befinden wir uns noch im ursprünglichen Aufruf von button1_Click, der auf dem Haupt-Thread stattfindet, der von der Nachrichtenschleife aufgerufen wurde. Wenn der Code, der zu await führt, viel Zeit in Anspruch nimmt, friert die Benutzeroberfläche immer noch ein. In unserem Beispiel nicht so sehr

  3. Was das Schlüsselwort await zusammen mit etwas cleverer Compilermagie bewirkt, ist, dass es im Grunde genommen so etwas wie "Ok, weißt du was? Ich werde einfach von der Schaltfläche zurückkehren, klicken Sie hier auf die Ereignisbehandlungsroutine Lassen Sie es mich wissen, da ich noch Code zur Ausführung habe ".

    Tatsächlich wird die SynchronizationContext-Klasse darüber informiert, dass dies geschehen ist. Je nach aktuellem Synchronisierungskontext wird die Ausführung in die Warteschlange gestellt. Die in einem Windows Forms-Programm verwendete Kontextklasse stellt sie mit der Warteschlange in die Warteschlange, die von der Nachrichtenschleife gepumpt wird.

  4. So kehrt es zur Nachrichtenschleife zurück, in der Sie nun weitere Nachrichten senden können, z. B. das Fenster verschieben, die Größe ändern oder auf andere Schaltflächen klicken.

    Für den Benutzer reagiert die Benutzeroberfläche jetzt wieder, verarbeitet andere Tastenklicks, ändert die Größe und, was am wichtigsten ist, zeichnet sie neu , so dass dies nicht der Fall ist scheint einzufrieren.

  5. 2 Sekunden später ist die Sache, auf die wir warten, abgeschlossen und was jetzt passiert, ist, dass es (nun, der Synchronisationskontext) eine Nachricht in die Warteschlange stellt, die die Nachrichtenschleife betrachtet, und sagt: "Hey, ich habe etwas mehr Code für you to execute ", und dieser Code ist der gesamte Code after the await.
  6. Wenn die Nachrichtenschleife zu dieser Nachricht gelangt, wird sie diese Methode im Grunde genommen dort "neu eingeben", wo sie aufgehört hat, unmittelbar nach await und den Rest der Methode ausführen. Beachten Sie, dass dieser Code erneut aus der Nachrichtenschleife aufgerufen wird. Wenn dieser Code also etwas Langes ausführt, ohne async/await Richtig zu verwenden, wird die Nachrichtenschleife erneut blockiert

Es gibt hier viele bewegliche Teile unter der Haube, daher hier einige Links zu weiteren Informationen. Ich wollte sagen, "sollten Sie es brauchen", aber dieses Thema ist ​​ziemlich breit und es ist ziemlich wichtig zu wissen einige dieser beweglichen Teile. Sie werden immer verstehen, dass async/await immer noch ein undichtes Konzept ist. Einige der zugrunde liegenden Einschränkungen und Probleme treten immer noch im umgebenden Code auf. Wenn dies nicht der Fall ist, müssen Sie in der Regel eine Anwendung debuggen, die scheinbar ohne triftigen Grund zufällig unterbrochen wird.


OK, und wenn GetSomethingAsync einen Thread startet, der in 2 Sekunden abgeschlossen ist? Ja, dann ist offensichtlich ein neuer Thread im Spiel. Dieser Thread ist jedoch nicht wegen der Asynchronität dieser Methode, sondern weil der Programmierer dieser Methode einen Thread ausgewählt hat, um asynchronen Code zu implementieren. Fast alle asynchronen E/A nicht ​​verwenden einen Thread, sie verwenden verschiedene Dinge. async/awaitfür sich Keine neuen Threads starten, aber offensichtlich können die "Dinge, auf die wir warten" mithilfe von Threads implementiert werden.

Es gibt viele Dinge in .NET, die einen Thread nicht unbedingt selbstständig starten, sondern immer noch asynchron sind:

  • Webanfragen (und viele andere netzwerkbezogene Dinge, die Zeit brauchen)
  • Asynchrones Lesen und Schreiben von Dateien
  • und vieles mehr ist ein gutes Zeichen, wenn die fragliche Klasse/Schnittstelle Methoden mit den Namen SomethingSomethingAsync oder BeginSomething und EndSomething hat und es sich um eine IAsyncResult handelt.

Normalerweise verwenden diese Dinge keinen Faden unter der Haube.


OK, also willst du etwas von diesem "breiten Themenkram"?

Fragen wir mal Try Roslyn nach unserem Button-Klick:

Probieren Sie Roslyn aus

Ich werde hier nicht die voll generierte Klasse verlinken, aber es ist ziemlich blödes Zeug.

Ich erkläre es ausführlich in meinem Blog-Beitrag There Is No Thread .

Zusammenfassend lässt sich festhalten, dass moderne E/A - Systeme häufig auf DMA (Direct Memory Access)) zugreifen. Auf Netzwerkkarten, Grafikkarten, Festplattencontrollern, seriellen/parallelen Schnittstellen usw Diese Prozessoren haben direkten Zugriff auf den Speicherbus und können unabhängig von der CPU lesen/schreiben. Die CPU muss nur das Gerät über den Speicherort informieren, der die Daten enthält, und kann dann seine eigene Aktion ausführen, bis das Gerät ausgelöst wird ein Interrupt, der die CPU darüber informiert, dass das Lesen/Schreiben abgeschlossen ist.

Sobald die Operation im Flug ist, muss die CPU keine Arbeit mehr erledigen und daher auch keinen Thread mehr.

83
Stephen Cleary

die einzige Möglichkeit für einen Computer, mehr als eine Aufgabe gleichzeitig zu erledigen, besteht darin, (1) tatsächlich mehr als eine Aufgabe gleichzeitig zu erledigen, (2) sie zu simulieren, indem Aufgaben geplant und zwischen ihnen gewechselt werden. Wenn async-await dies nicht tut

Es ist nicht so, dass das Warten auch nicht von denen tut. Denken Sie daran, dass der Zweck von await nicht darin besteht, synchronen Code magisch asynchron zu machen. Es ist zu aktivieren mit den gleichen Techniken, die wir zum Schreiben von synchronem Code beim Aufruf in asynchronen Code verwenden. Beim Warten geht es um den Code, der Operationen mit hoher Latenz verwendet, wie Code aussehen zu lassen, der Operationen mit niedriger Latenz verwendet. Diese Operationen mit hoher Latenz können sich auf Threads, auf Spezialhardware, auf Zerlegen ihrer Arbeit in kleine Stücke und das spätere Einreihen in die Nachrichtenwarteschlange für die Verarbeitung durch den UI-Thread beziehen. Sie tun etwas, um Asynchronität zu erreichen, aber sie sind diejenigen, die es tun. Mit Await können Sie diese Asynchronität nur ausnutzen.

Ich denke auch, dass Ihnen eine dritte Option fehlt. Wir alten Leute - Kinder heute mit ihrer Rap-Musik sollten von meinem Rasen verschwinden usw. - erinnern uns an die Welt von Windows in den frühen 1990ern. 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. Wenn sich dieser Prozess schlecht verhält, werden alle anderen Prozesse nicht mehr bedient. Es läuft, bis es nachgibt, und muss irgendwie wissen, wie man dort weitermacht, wo es aufgehört hat, wenn die OS-Hände das nächste Mal wieder darauf zugreifen. Asynchroner Code mit einem Thread ist ähnlich, mit "wait" anstelle von "yield". Warten bedeutet: "Ich werde mich daran erinnern, wo ich aufgehört habe, und eine Weile jemanden laufen lassen. Rufen Sie mich zurück, wenn die Aufgabe, auf die ich gewartet habe, abgeschlossen ist, und ich werde dort weitermachen, wo ich aufgehört habe." Ich denke, Sie können sehen, wie das Apps reaktionsschneller macht, so wie es in den Windows-3-Tagen getan hat.

das Aufrufen einer Methode bedeutet, auf den Abschluss der Methode zu warten

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 besagt, dass diese Arbeit gerade ausgeführt wird. Sagen Sie mir, was zu tun ist, wenn sie abgeschlossen ist. Die Arbeit der Methode ist noch nicht erledigt, obwohl sie zurückgekehrt ist.

Vor dem wait-Operator mussten Sie Code schreiben, der wie Spaghetti aussah, die durch Schweizer Käse gezogen wurden, um die Tatsache zu bewältigen, dass wir nach Abschluss, aber mit der Rückkehr und dem Abschluss desynchronisiert arbeiten müssen. Await ermöglicht es Ihnen, Code zu schreiben, der sieht aus so aussieht, als ob die Rückgabe und die Vervollständigung synchronisiert sind, ohne dass sie tatsächlich synchronisiert werden.

82
Eric Lippert

Ich bin wirklich froh, dass jemand diese Frage gestellt hat, weil ich die längste Zeit glaubte, dass Threads für die Parallelität notwendig waren. Als ich zum ersten Mal Ereignisschleifen sah , hielt ich sie für 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 von verschiedenen Kerneln unter verschiedenen Namen: select(), poll(), epoll(), kqueue(). Hierbei handelt es sich um Systemaufrufe , die es Ihnen ermöglichen, eine Reihe von Dateideskriptoren zu überwachen, obwohl sich die Implementierungsdetails unterscheiden der beobachteten Dateideskriptoren ändert sich.

Auf diese Weise kann man auf eine Reihe von IO - Ereignissen (die Hauptereignisschleife)) warten, das erste abgeschlossene Ereignis behandeln und dann die Kontrolle an die Ereignisschleife zurückgeben.

Wie funktioniert das? Die kurze Antwort lautet: Kernel- und Hardware-Magie. Neben der CPU gibt es in einem Computer viele Komponenten, die parallel arbeiten können. 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 von Single-Threaded-Ereignisschleifen wie node.js oder Tornado. Wenn Sie eine Funktion await, beobachten Sie ein bestimmtes Ereignis Wenn das Ereignis, das Sie gerade ansehen, beendet ist, wird die Funktion (irgendwann) dort fortgesetzt, wo sie aufgehört hat. Funktionen, mit denen Sie die Berechnung wie folgt anhalten und fortsetzen können genannt Coroutinen .

27
gardenhead

await und async verwenden Aufgaben keine Threads.

Das Framework verfügt über einen Pool von Threads, die bereit sind, einige Arbeiten in Form von Task -Objekten auszuführen. Einreichen einer Aufgabe in den Pool bedeutet die Auswahl einer freien, bereits vorhandenen1, Thread zum Aufrufen der Taskaktionsmethode.
Erstellen einer Aufgabe ist die Aufgabe, ein neues Objekt zu erstellen, viel schneller als das Erstellen eines neuen Threads.

Wenn eine Task gegeben ist, kann eine Fortsetzung daran angehängt werden. Dies ist ein neues Task Objekt, das ausgeführt wird, sobald der Thread endet .

Schon seit async/await benutze Task s nicht erstelle einen neuen Thread.


Obwohl die Interrupt-Programmiertechnik in jedem modernen Betriebssystem weit verbreitet ist, halte ich sie hier nicht für relevant.
Sie können zwei CPU-gebundene Tasks parallel ausführen (tatsächlich verschachtelt) in einer einzelnen CPU mit aysnc/await.
Das lässt sich nicht einfach damit erklären, dass das Betriebssystem Warteschlangen unterstützt [~ # ~] iorp [~ # ~].


Als ich das letzte Mal den Compiler überprüft habe, der async -Methoden in DFA transformiert hat, ist die Arbeit in Schritte unterteilt, die jeweils mit einem await Anweisung.
Der await startet seine Task und hängt eine Fortsetzung an, um den nächsten Schritt auszuführen.

Als ein Konzeptbeispiel ist hier ein Pseudocodebeispiel.
Der Übersichtlichkeit halber und weil ich mich nicht genau an alle Details erinnere, werden die Dinge vereinfacht.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

Es wird in so etwas verwandelt

int state = 0;

Task NeXTSTEP()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(NeXTSTEP());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(NeXTSTEP());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   NeXTSTEP();

1 Tatsächlich kann ein Pool eine Richtlinie zum Erstellen von Aufgaben haben.

22
Margaret Bloom

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 alleine verwenden, reagiert Ihre App nicht auf magische Weise. Wenn alles, was Sie in der Methode tun, auf die Sie warten, von der Benutzeroberfläche blockiert wird, die Benutzeroberfläche wird weiterhin auf die gleiche Weise blockiert wie bei der nicht erwarteten Version.

Sie müssen Ihre erwartete Methode speziell schreiben, damit sie entweder einen neuen Thread erzeugt oder so etwas wie einen Abschluss-Port verwendet (der die Ausführung im aktuellen Thread zurückgibt und etwas anderes zur Fortsetzung aufruft, wenn der Abschluss-Port signalisiert wird). Aber dieser Teil wird in anderen Antworten gut erklärt.

15
Andrew Savinykh

Hier ist, wie ich das alles sehe, es ist vielleicht nicht super technisch genau, aber es hilft mir zumindest :).

Grundsätzlich gibt es zwei Arten der Verarbeitung (Berechnung), die auf einer Maschine stattfinden:

  • verarbeitung, die auf der CPU passieren
  • wenn die Verarbeitung auf anderen Prozessoren (GPU, Netzwerkkarte usw.) stattfindet, bezeichnen wir sie als E/A.

Wenn wir also nach dem Kompilieren einen Teil des Quellcodes schreiben, wird die Verarbeitung abhängig vom verwendeten Objekt (und das ist sehr wichtig) CPU-gebunden oder IO-gebunden sein = und tatsächlich kann es an eine Kombination von beiden gebunden sein.

Einige Beispiele:

  • wenn ich die Write-Methode des FileStream -Objekts (bei dem es sich um einen Stream handelt) verwende, wird die Verarbeitung zu 1% CPU-gebunden und zu 99% IO gebunden).
  • wenn ich die Write-Methode des NetworkStream -Objekts (bei dem es sich um einen Stream handelt) verwende, wird die Verarbeitung zu 1% CPU-gebunden und zu 99% IO gebunden).
  • wenn ich die Write-Methode des Memorystream -Objekts (bei dem es sich um einen Stream handelt) verwende, wird die Verarbeitung zu 100% an die CPU gebunden.

Aus der Sicht eines objektorientierten Programmierers hängt das, was darunter passiert, möglicherweise stark vom endgültigen Typ des Objekts ab, obwohl ich immer auf ein Stream -Objekt zugreife.

Um die Dinge zu optimieren, ist es manchmal nützlich, Code parallel auszuführen (Anmerkung: Ich verwende das Wort nicht asynchron), wenn dies möglich ist und/oder notwendig.

Einige Beispiele:

  • In einer Desktop-App möchte ich ein Dokument drucken, aber nicht darauf warten.
  • Mein Webserver bedient viele Clients gleichzeitig, wobei jeder seine Seiten parallel erhält (nicht serialisiert).

Vor dem asynchronen Abwarten hatten wir im Wesentlichen zwei Lösungen:

  • Themen. Mit Thread- und ThreadPool-Klassen war die Verwendung relativ einfach. Threads sind nur an die CPU gebunden.
  • Das "alte" Begin/End/AsyncCallback asynchrones Programmiermodell. Es ist nur ein Modell, es sagt Ihnen nicht, ob Sie CPU oder IO gebunden sind. Wenn Sie sich die Socket - oder FileStream - Klassen ansehen, ist es IO gebunden, was cool ist, aber wir benutzen es selten.

Das async/await ist nur ein gemeinsames Programmiermodell, basierend auf dem Task-Konzept. Es ist ein bisschen einfacher zu verwenden als Threads oder Thread-Pools für CPU-gebundene Aufgaben und viel einfacher zu verwenden als das alte Start-/End-Modell. Undercovers, aber es ist "nur" ein super ausgefeilter Wrapper für beide Funktionen.

Also der eigentliche Gewinn ist meistens auf IO Bound Tasks, Task, die nicht die CPU verwendet, aber async/await ist immer noch nur ein Programmiermodell, tut es nicht hilft dir nicht zu bestimmen, wie/wo die Verarbeitung am Ende stattfinden wird.

Es bedeutet nicht, dass eine Klasse eine Methode "DoSomethingAsync" hat, die ein Task-Objekt zurückgibt. Sie können davon ausgehen, dass es CPU-gebunden ist (was bedeutet, dass es möglicherweise völlig nutzlos ist . ), insbesondere wenn es keinen Stornierungsparameter hat) oder IO Bound (was bedeutet, dass es wahrscheinlich ein muss ) oder eine Kombination aus beidem (da das Modell ziemlich viral ist, können Bindungen und potenzielle Vorteile letztendlich sehr gemischt und nicht so offensichtlich sein).

Zurück zu meinen Beispielen: Wenn ich meine Schreibvorgänge mit async/await auf MemoryStream durchführe, bleibt die CPU gebunden (ich werde wahrscheinlich nicht davon profitieren), obwohl ich mit Sicherheit von Dateien und Netzwerk-Streams profitieren werde.

13
Simon Mourier

Andere Antworten zusammenfassen:

Async/await wird hauptsächlich für IO gebundene Tasks erstellt, da durch deren Verwendung das Blockieren des aufrufenden Threads vermieden werden kann. Ihre Hauptverwendung liegt bei UI-Threads, bei denen das Blockieren des Threads nicht erwünscht ist bei einer IO gebundenen Operation.

Async erstellt keinen eigenen Thread. Der Thread der aufrufenden Methode wird verwendet, um die asynchrone Methode auszuführen, bis eine erwartete gefunden wird. Derselbe 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 von der erwarteten auf einen Thread aus dem Thread-Pool ausgeführt werden - der einzige Ort, an dem ein separater Thread ins Bild kommt.

2
vaibhav kumar

Ich versuche es von Grund auf zu erklären. Vielleicht findet es jemand hilfreich. Ich war dabei, habe das getan, habe es neu erfunden, als ich einfache Spiele in DOS in Pascal gemacht habe (gute alte Zeiten ...)

Also ... In jeder ereignisgesteuerten Anwendung befindet sich eine Ereignisschleife, die ungefähr so ​​aussieht:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

Frameworks verbergen dieses Detail normalerweise vor Ihnen, aber es ist vorhanden. Die Funktion getMessage liest das nächste Ereignis aus der Ereigniswarteschlange oder wartet, bis ein Ereignis eintritt: Bewegen der Maus, Herunterfahren, Hochfahren, Klicken usw. Anschließend sendet dispatchMessage das Ereignis an den entsprechenden Ereignishandler. Wartet dann auf das nächste Ereignis usw., bis ein Beendigungsereignis eintritt, das die Schleifen verlässt und die Anwendung beendet.

Ereignishandler sollten schnell ausgeführt werden, damit die Ereignisschleife nach weiteren Ereignissen suchen kann und die Benutzeroberfläche weiterhin reagiert. Was passiert, wenn ein Klick auf eine Schaltfläche eine so kostspielige Operation auslöst?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

Nun, die Benutzeroberfläche reagiert nicht mehr, bis der 10-Sekunden-Vorgang beendet 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 alles in einem einzigen Ereignis erledigen können. Sie müssen einen kleinen Teil der Arbeit erledigen und dann ein anderes Ereignis posten in die Ereigniswarteschlange stellen, um die Fortsetzung anzufordern.

Also würden Sie dies ändern zu:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

In diesem Fall wird nur die erste Iteration ausgeführt. Anschließend wird eine Nachricht an die Ereigniswarteschlange gesendet, um die nächste Iteration auszuführen, und es wird zurückgegeben. In unserem Beispiel wird mit der Pseudofunktion postFunctionCallMessage 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 fortlaufend Teile einer langen Arbeit ausgeführt werden.

Solange diese lang laufende Task ausgeführt wird, befindet sich ihr Fortsetzungsereignis immer in der Ereigniswarteschlange. Sie haben also im Grunde Ihren eigenen Aufgabenplaner erfunden. Wo die Fortsetzungsereignisse in der Warteschlange "Prozesse" sind, die ausgeführt werden. Tatsächlich tun dies die Betriebssysteme, mit der Ausnahme, dass das Senden der Fortsetzungsereignisse und die Rückkehr zur Scheduler-Schleife über den Timer-Interrupt der CPU erfolgt, in dem das Betriebssystem den Kontextumschaltcode registriert hat, sodass Sie sich nicht darum kümmern müssen. Aber hier schreiben Sie Ihren eigenen Scheduler, damit Sie sich darum kümmern müssen - bis jetzt.

Auf diese Weise können wir in einem einzigen Thread parallel zur GUI lang laufende Aufgaben ausführen, indem wir sie in kleine Abschnitte aufteilen und Fortsetzungsereignisse senden. Dies ist die allgemeine Idee der Klasse Task. Es stellt eine Arbeit dar und wenn Sie .ContinueWith Darauf aufrufen, 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). Die Klasse Task verwendet einen Thread-Pool, in dem in jedem Thread eine Ereignisschleife vorhanden ist, die darauf wartet, Aufgaben auszuführen, die ich am Anfang 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 richtig in kleine Teile aufgeteilt sind, scheint jeder von ihnen parallel zu laufen.

Aber all diese Verkettung, bei der die Arbeit manuell in kleine Stücke zerlegt wird, ist eine mühsame Arbeit, die das Layout der Logik völlig durcheinander bringt, da der gesamte Code der Hintergrundaufgabe im Grunde genommen ein .ContinueWith - Durcheinander ist. Hier hilft Ihnen der Compiler. Es erledigt all diese Verkettung und Fortsetzung für Sie im Hintergrund. Wenn Sie await sagen, teilen Sie dem Compiler mit, 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.

1
Calmarius

Tatsächlich, async await-Ketten sind vom CLR-Compiler generierte Zustandsmaschinen.

async await verwendet jedoch Threads, die TPL zum Ausführen von Tasks verwendet.

Der Grund, warum die Anwendung nicht blockiert wird, besteht darin, dass die Zustandsmaschine entscheiden kann, welche Co-Routine ausgeführt, wiederholt, geprüft und erneut entschieden werden soll.

Weitere Lektüre:

Was erzeugt async & await?

Async Await und die generierte StateMachine

Asynchrones C # und F # (III.): Wie funktioniert es? - Tomas Petricek

Edit:

Okay. Es scheint, als wäre meine Ausarbeitung falsch. Ich muss jedoch darauf hinweisen, dass Zustandsautomaten ein wichtiges Kapital für async awaits. Auch wenn Sie asynchrone E/A-Vorgänge ausführen, benötigen Sie einen Helfer, der überprüft, ob die Operation abgeschlossen ist. Daher benötigen wir weiterhin eine Zustandsmaschine, mit der ermittelt werden kann, welche Routine asynchron zusammen ausgeführt werden kann.

0
Steve Fan