Was ist der Unterschied zwischen asynchroner Programmierung und Multithreading?

234

Ich dachte, dass sie im Grunde dasselbe sind - Programme schreiben, die Aufgaben zwischen Prozessoren aufteilen (auf Maschinen mit 2+ Prozessoren). Dann lese ich das , das sagt:

Asynchrone Methoden sollen nicht blockierende Vorgänge sein. Ein Warte-Ausdruck in einer asynchronen Methode blockiert den aktuellen Thread nicht, während die erwartete Aufgabe ausgeführt wird. Stattdessen meldet der Ausdruck den Rest der Methode als Fortsetzung und gibt die Kontrolle an den Aufrufer der asynchronen Methode zurück.

Die Schlüsselwörter async und await führen nicht dazu, dass zusätzliche Threads erstellt werden. Asynchrone Methoden erfordern kein Multithreading, da eine asynchrone Methode nicht in einem eigenen Thread ausgeführt wird. Die Methode wird im aktuellen Synchronisationskontext ausgeführt und verwendet die Zeit im Thread nur, wenn die Methode aktiv ist. Sie können Task.Run verwenden, um CPU-gebundene Arbeit in einen Hintergrundthread zu verschieben. Ein Hintergrundthread hilft jedoch nicht bei einem Prozess, der nur darauf wartet, dass Ergebnisse verfügbar werden.

und ich frage mich, ob jemand das für mich ins Englische übersetzen kann. Es scheint einen Unterschied zwischen Asynkronizität (ist das ein Wort?) Und Threading zu machen und zu implizieren, dass Sie ein Programm haben können, das asynchrone Aufgaben, aber kein Multithreading hat.

Jetzt verstehe ich die Idee von asynchronen Aufgaben wie dem Beispiel auf Seite. 467 von Jon Skeets C # In Depth, dritte Ausgabe

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

Das asyncSchlüsselwort bedeutet " Diese Funktion wird, wann immer sie aufgerufen wird, nicht in einem Kontext aufgerufen, in dem ihre Fertigstellung erforderlich ist, damit nach dem Aufruf alles aufgerufen wird."

Mit anderen Worten, schreiben Sie es mitten in einer Aufgabe

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, da DisplayWebsiteLength()hat nichts mit xoder zu tun y, wird DisplayWebsiteLength()"im Hintergrund" ausgeführt, wie

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Das ist natürlich ein dummes Beispiel, aber bin ich richtig oder total verwirrt oder was?

(Außerdem bin ich verwirrt darüber, warum senderund werde eim Hauptteil der obigen Funktion nie verwendet.)

user5648283
quelle
13
Dies ist eine schöne Erklärung: blog.stephencleary.com/2013/11/there-is-no-thread.html
Jakub Lortz
senderund eschlagen vor, dass dies tatsächlich ein Event-Handler ist - so ziemlich der einzige Ort, an dem async voides wünschenswert ist. Dies wird höchstwahrscheinlich durch Klicken auf eine Schaltfläche oder ähnliches aufgerufen. Das Ergebnis ist, dass diese Aktion in Bezug auf den Rest der Anwendung vollständig asynchron ausgeführt wird. Aber es ist immer noch alles in einem Thread - dem UI-Thread (mit einem winzigen Zeitabschnitt in einem IOCP-Thread, der den Rückruf an den UI-Thread sendet).
Luaan
3
Ein sehr wichtiger Hinweis zum DisplayWebsiteLengthCodebeispiel: Sie sollten ihn nicht HttpClientin einer usingAnweisung verwenden. Unter hoher Last kann der Code die Anzahl der verfügbaren Sockets erschöpfen, was zu SocketException-Fehlern führt. Weitere Informationen zu Unsachgemäßer Instanziierung .
Gan
1
@ JakubLortz Ich weiß nicht, für wen der Artikel wirklich ist. Nicht für Anfänger, da gute Kenntnisse über Threads, Interrupts, CPU-bezogene Dinge usw. erforderlich sind. Nicht für fortgeschrittene Benutzer, da für sie bereits alles klar ist. Ich bin sicher, es wird niemandem helfen zu verstehen, worum es geht - zu viel Abstraktion.
Loreno

Antworten:

588

Ihr Missverständnis ist äußerst häufig. Vielen Menschen wird beigebracht, dass Multithreading und Asynchronität dasselbe sind, aber nicht.

Eine Analogie hilft normalerweise. Sie kochen in einem Restaurant. Eier und Toast werden bestellt.

  • Synchron: Sie kochen die Eier, dann kochen Sie den Toast.
  • Asynchron, Single Threaded: Sie starten das Kochen der Eier und stellen einen Timer ein. Sie starten das Toastkochen und stellen einen Timer ein. Während beide kochen, putzen Sie die Küche. Wenn die Timer ablaufen, nehmen Sie die Eier vom Herd und den Toast aus dem Toaster und servieren sie.
  • Asynchron, Multithreaded: Sie stellen zwei weitere Köche ein, einen zum Kochen von Eiern und einen zum Kochen von Toast. Jetzt haben Sie das Problem, die Köche so zu koordinieren, dass sie beim Teilen von Ressourcen in der Küche nicht miteinander in Konflikt geraten. Und du musst sie bezahlen.

Ist es nun sinnvoll, dass Multithreading nur eine Art von Asynchronität ist? Beim Threading geht es um Arbeiter; Bei Asynchronität geht es um Aufgaben . In Multithread-Workflows weisen Sie Arbeitern Aufgaben zu. In asynchronen Single-Threaded-Workflows haben Sie ein Diagramm mit Aufgaben, bei denen einige Aufgaben von den Ergebnissen anderer abhängen. Wenn jede Aufgabe abgeschlossen ist, wird der Code aufgerufen, der die nächste Aufgabe plant, die ausgeführt werden kann, wenn die Ergebnisse der gerade abgeschlossenen Aufgabe vorliegen. Aber Sie brauchen (hoffentlich) nur einen Arbeiter, um alle Aufgaben auszuführen, nicht einen Arbeiter pro Aufgabe.

Es wird hilfreich sein zu erkennen, dass viele Aufgaben nicht prozessorgebunden sind. Für prozessorgebundene Aufgaben ist es sinnvoll, so viele Worker (Threads) einzustellen, wie es Prozessoren gibt, jedem Worker eine Aufgabe zuzuweisen, jedem Worker einen Prozessor zuzuweisen und jeden Prozessor die Aufgabe zu übernehmen, nur das Ergebnis als zu berechnen so schnell wie möglich. Für Aufgaben, die nicht auf einen Prozessor warten, müssen Sie jedoch überhaupt keinen Mitarbeiter zuweisen. Sie warten nur auf die Nachricht, dass das Ergebnis verfügbar ist, und tun etwas anderes, während Sie warten . Wenn diese Nachricht eintrifft, können Sie die Fortsetzung der abgeschlossenen Aufgabe als nächstes auf Ihrer Aufgabenliste zum Abhaken planen.

Schauen wir uns Jons Beispiel genauer an. Was geschieht?

  • Jemand ruft DisplayWebSiteLength auf. WHO? Es ist uns egal.
  • Es legt eine Beschriftung fest, erstellt einen Client und fordert den Client auf, etwas abzurufen. Der Client gibt ein Objekt zurück, das die Aufgabe darstellt, etwas abzurufen. Diese Aufgabe ist im Gange.
  • Ist es in einem anderen Thread in Bearbeitung? Wahrscheinlich nicht. Lesen Sie Stephens Artikel darüber, warum es keinen Thread gibt.
  • Jetzt warten wir auf die Aufgabe. Was geschieht? Wir überprüfen, ob die Aufgabe zwischen dem Zeitpunkt, zu dem wir sie erstellt haben, und dem Warten abgeschlossen ist. Wenn ja, holen wir das Ergebnis und laufen weiter. Nehmen wir an, es ist noch nicht abgeschlossen. Wir melden den Rest dieser Methode als Fortsetzung dieser Aufgabe an und kehren zurück .
  • Jetzt ist die Kontrolle an den Anrufer zurückgekehrt. Was tut es? Was auch immer es will.
  • Angenommen, die Aufgabe ist abgeschlossen. Wie hat es das gemacht? Vielleicht lief es auf einem anderen Thread, oder vielleicht hat der Aufrufer, zu dem wir gerade zurückgekehrt sind, zugelassen, dass es auf dem aktuellen Thread vollständig ausgeführt wird. Unabhängig davon haben wir jetzt eine abgeschlossene Aufgabe.
  • Die abgeschlossene Aufgabe fordert den richtigen Thread - wahrscheinlich wieder den einzigen Thread - auf, die Fortsetzung der Aufgabe auszuführen.
  • Die Kontrolle kehrt sofort zu der Methode zurück, die wir gerade am Punkt des Wartens verlassen haben. Jetzt ist ein Ergebnis verfügbar, mit dem wir textden Rest der Methode zuweisen und ausführen können .

Es ist genau wie in meiner Analogie. Jemand fragt Sie nach einem Dokument. Sie senden die E-Mail für das Dokument ab und erledigen weitere Arbeiten. Wenn es in der Post ankommt, werden Sie benachrichtigt, und wenn Sie Lust dazu haben, erledigen Sie den Rest des Workflows - öffnen Sie den Umschlag, zahlen Sie die Zustellgebühren, was auch immer. Sie müssen keinen anderen Mitarbeiter einstellen, um das alles für Sie zu erledigen.

Eric Lippert
quelle
8
@ user5648283: Die Hardware ist die falsche Ebene, um über Aufgaben nachzudenken. Eine Aufgabe ist einfach ein Objekt, das (1) darstellt, dass ein Wert in Zukunft verfügbar sein wird, und (2) Code (auf dem richtigen Thread) ausführen kann, wenn dieser Wert verfügbar ist . Wie eine einzelne Aufgabe in Zukunft zum Ergebnis führt, hängt davon ab. Einige verwenden dafür spezielle Hardware wie "Festplatten" und "Netzwerkkarten". Einige verwenden Hardware wie CPUs.
Eric Lippert
13
@ user5648283: Denken Sie noch einmal über meine Analogie nach. Wenn Sie jemand bittet, Eier und Toast zu kochen, verwenden Sie spezielle Hardware - einen Herd und einen Toaster - und Sie können die Küche reinigen, während die Hardware ihre Arbeit erledigt. Wenn Sie jemand nach Eiern, Toast und einer Originalkritik des letzten Hobbit-Films fragt, können Sie Ihre Bewertung schreiben, während die Eier und der Toast kochen, aber Sie müssen dafür keine Hardware verwenden.
Eric Lippert
9
@ user5648283: Nun zu Ihrer Frage zum "Neuanordnen des Codes". Angenommen, Sie haben eine Methode P mit einer Ertragsrendite und eine Methode Q, die das Ergebnis von P überprüft. Gehen Sie den Code durch. Sie werden sehen, dass wir ein bisschen Q, dann ein bisschen P und dann ein bisschen Q ausführen ... Verstehst du den Sinn davon? Warten ist im Wesentlichen Rendite Rendite in Kostüm . Ist es jetzt klarer?
Eric Lippert
10
Der Toaster ist Hardware. Hardware benötigt keinen Thread, um sie zu warten. Festplatten und Netzwerkkarten und so weiter laufen weit unter dem von Betriebssystem-Threads.
Eric Lippert
5
@ ShivprasadKoirala: Das ist überhaupt nicht wahr . Wenn Sie das glauben, dann haben Sie einige sehr falsche Vorstellungen über Asynchronität . Der springende Punkt bei der Asynchronität in C # ist, dass kein Thread erstellt wird.
Eric Lippert
27

In-Browser-Javascript ist ein großartiges Beispiel für ein asynchrones Programm ohne Threads.

Sie müssen sich nicht darum kümmern, dass mehrere Codeteile gleichzeitig dieselben Objekte berühren: Jede Funktion wird beendet, bevor ein anderes Javascript auf der Seite ausgeführt werden darf.

Wenn Sie jedoch so etwas wie eine AJAX-Anfrage ausführen, wird überhaupt kein Code ausgeführt, sodass anderes Javascript auf Dinge wie Klickereignisse reagieren kann, bis diese Anfrage zurückkommt und den damit verbundenen Rückruf aufruft. Wenn einer dieser anderen Ereignishandler noch ausgeführt wird, wenn die AJAX-Anforderung zurückkommt, wird sein Handler erst aufgerufen, wenn sie fertig sind. Es wird nur ein JavaScript- "Thread" ausgeführt, obwohl Sie das, was Sie getan haben, effektiv anhalten können, bis Sie die benötigten Informationen haben.

In C # -Anwendungen geschieht dasselbe immer dann, wenn Sie sich mit UI-Elementen befassen. Sie dürfen nur dann mit UI-Elementen interagieren, wenn Sie sich im UI-Thread befinden. Wenn der Benutzer auf eine Schaltfläche geklickt hat und Sie eine große Datei von der Festplatte lesen möchten, kann ein unerfahrener Programmierer den Fehler machen, die Datei im Click-Event-Handler selbst zu lesen, was dazu führen würde, dass die Anwendung bis zum "Einfrieren" Das Laden der Datei wurde beendet, da auf weitere Klick-, Schwebeflug- oder andere UI-bezogene Ereignisse erst reagiert werden darf, wenn dieser Thread freigegeben wurde.

Eine Option, die Programmierer verwenden können, um dieses Problem zu vermeiden, besteht darin, einen neuen Thread zum Laden der Datei zu erstellen und dann dem Code des Threads mitzuteilen, dass beim Laden der Datei der verbleibende Code erneut auf dem UI-Thread ausgeführt werden muss, damit UI-Elemente aktualisiert werden können basierend auf dem, was es in der Datei gefunden hat. Bis vor kurzem war dieser Ansatz sehr beliebt, da er durch die C # -Bibliotheken und die Sprache vereinfacht wurde, aber grundlegend komplizierter ist, als er sein muss.

Wenn Sie darüber nachdenken, was die CPU tut, wenn sie eine Datei auf der Ebene der Hardware und des Betriebssystems liest, gibt sie im Grunde eine Anweisung aus, Daten von der Festplatte in den Speicher zu lesen und das Betriebssystem mit einem "Interrupt" zu treffen "wenn der Lesevorgang abgeschlossen ist. Mit anderen Worten, das Lesen von der Festplatte (oder einer beliebigen E / A) ist von Natur aus eine asynchrone Operation. Das Konzept eines Threads, der darauf wartet, dass diese E / A abgeschlossen wird, ist eine Abstraktion, die die Bibliotheksentwickler erstellt haben, um das Programmieren zu vereinfachen. Es ist nicht nötig.

Jetzt haben die meisten E / A-Vorgänge in .NET eine entsprechende ...Async()Methode, die Sie aufrufen können und die Taskfast sofort eine zurückgibt . Sie können Rückrufe hinzufügen Task, um den Code anzugeben, den Sie ausführen möchten, wenn der asynchrone Vorgang abgeschlossen ist. Sie können auch angeben, auf welchem ​​Thread dieser Code ausgeführt werden soll, und Sie können ein Token bereitstellen, das der asynchrone Vorgang von Zeit zu Zeit überprüfen kann, um festzustellen, ob Sie die asynchrone Aufgabe abbrechen möchten, damit sie ihre Arbeit schnell beenden kann und anmutig.

Bis zum async/awaitHinzufügen der Schlüsselwörter war C # viel offensichtlicher darüber, wie Rückrufcode aufgerufen wird, da diese Rückrufe in Form von Delegaten vorliegen, die Sie der Aufgabe zugeordnet haben. Um Ihnen weiterhin den Vorteil der Verwendung der ...Async()Operation zu bieten und gleichzeitig die Komplexität des Codes zu vermeiden, async/awaitwird die Erstellung dieser Delegaten abstrahiert. Aber sie sind immer noch im kompilierten Code enthalten.

So kann Ihr UI-Ereignishandler awaiteine E / A-Operation ausführen, den UI-Thread für andere Aufgaben freigeben und nach dem Lesen der Datei mehr oder weniger automatisch zum UI-Thread zurückkehren - ohne dies jemals tun zu müssen Erstelle einen neuen Thread.

StriplingWarrior
quelle
Es wird nur ein JavaScript- "Thread" ausgeführt - bei Web Workern nicht mehr .
Oleksii
6
@oleksii: Das ist technisch gesehen richtig, aber ich wollte nicht darauf eingehen, da die Web Workers-API selbst asynchron ist und Web Worker die Javascript-Werte oder das DOM auf der von ihnen aufgerufenen Webseite nicht direkt beeinflussen dürfen von, was bedeutet, dass der entscheidende zweite Absatz dieser Antwort immer noch gilt. Aus Sicht des Programmierers gibt es kaum einen Unterschied zwischen dem Aufrufen eines Web Workers und dem Aufrufen einer AJAX-Anforderung.
StriplingWarrior