Was ist der Unterschied zwischen asynchroner Programmierung und Multithreading?

Ich dachte, dass sie im Grunde dasselbe waren – Programme zu schreiben, die Aufgaben zwischen processoren aufteilen (auf Maschinen, die 2+ processoren haben). Dann lese ich https://msdn.microsoft.com/en-us/library/hh191443.aspx , was sagt

Async-Methoden sollen nicht blockierende Operationen sein. Ein erwarteter Ausdruck in einer asynchronen Methode blockiert den aktuellen Thread nicht, während die erwartete Aufgabe ausgeführt wird. Stattdessen gibt der Ausdruck den Rest der Methode als Fortsetzung an und gibt die Steuerung an den Aufrufer der asynchronen Methode zurück.

Die Schlüsselwörter async und await verursachen keine zusätzlichen Threads. Async-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 dann, wenn die Methode aktiv ist. Sie können Task.Run verwenden, um CPU-gebundene Arbeit in einen Hintergrundthread zu verschieben, aber ein Hintergrundthread hilft nicht bei einem process, 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 Asynchronität (ist das ein Wort?) Und Threading zu machen und impliziert, dass Sie ein Programm haben können, das asynchrone Aufgaben, aber kein Multithreading hat.

Jetzt verstehe ich die Idee von asynchronen Aufgaben wie das Beispiel auf pg. 467 von Jon Skeets C # In Depth, Third Edition

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

Das async Schlüsselwort bedeutet ” Diese function wird, wenn sie aufgerufen wird, nicht in einem Kontext aufgerufen, in dem ihre Beendigung für alles erforderlich ist, nachdem ihr Aufruf aufgerufen wurde.”

Mit anderen Worten, es in die Mitte einer Aufgabe schreiben

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

, da DisplayWebsiteLength() nichts mit x oder y zu tun hat, wird DisplayWebsiteLength() “im Hintergrund” ausgeführt

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

Offensichtlich ist das ein dummes Beispiel, aber habe ich Recht oder bin ich total verwirrt oder was?

(Außerdem bin ich verwirrt darüber, warum sender und e niemals im Körper der obigen function verwendet werden.)

   

Dein Missverständnis ist sehr häufig. Vielen Leuten wird beigebracht, dass Multithreading und Asynchronie dasselbe sind, aber sie sind es nicht.

Eine Analogie hilft normalerweise. Sie kochen in einem Restaurant. Eine Bestellung kommt für Eier und Toast.

  • Synchron: Sie kochen die Eier, dann kochen Sie den Toast.
  • Asynchron, single threaded: Sie starten das Eierkochen und stellen einen Timer ein. Sie starten das Toastkochen und stellen einen Timer ein. Während sie beide kochen, putzt du die Küche. Wenn die Timer losgehen, nehmen Sie die Eier von der Hitze und den Toast aus dem Toaster und servieren Sie ihnen.
  • Asynchron, Multithread: Sie stellen zwei weitere Köche ein, einen zum Eierkochen und einen zum Toastkochen. Jetzt haben Sie das Problem, die Köche so zu koordinieren, dass sie beim Teilen von Ressourcen nicht in der Küche miteinander in Konflikt geraten. Und du musst sie bezahlen.

Nun macht es Sinn, dass Multithreading nur eine Art von Asynchronität ist? Threading dreht sich um Arbeiter; Asynchronität geht 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 die einzelnen Aufgaben abgeschlossen sind, ruft sie den Code auf, der die nächste auszuführende Aufgabe aufgrund der Ergebnisse der gerade abgeschlossenen Aufgabe plant. Aber Sie benötigen (hoffentlich) nur einen Arbeiter, um alle Aufgaben auszuführen, nicht einen Arbeiter pro Aufgabe.

Es hilft zu erkennen, dass viele Aufgaben nicht prozessorgebunden sind. Für prozessorgebundene Tasks ist es sinnvoll, so viele Worker (Threads) einzustellen, wie processoren vorhanden sind, jedem Worker eine Task zuzuweisen, jedem Worker einen processor zuzuweisen und jedem processor die Aufgabe zu übertragen, nur das Ergebnis zu berechnen so schnell wie möglich. Bei Aufgaben, die nicht auf einem processor warten, müssen Sie keinen Mitarbeiter zuweisen. Sie warten nur auf die Nachricht, dass das Ergebnis verfügbar ist, und tun Sie etwas anderes, während Sie warten . Wenn diese Nachricht eintrifft, können Sie die Fortsetzung der abgeschlossenen Aufgabe als nächsten Schritt auf Ihrer To-Do-Liste planen.

Sehen wir uns Jon’s Beispiel genauer an. Was geschieht?

  • Jemand ruft DisplayWebSiteLength auf. Wer? Uns ist es egal.
  • Er setzt ein Label, erstellt einen Client und fordert den Client auf, etwas zu holen. Der Client gibt ein Objekt zurück, das die Aufgabe darstellt, etwas abzurufen. Diese Aufgabe ist in Bearbeitung.
  • Ist es in einem anderen Thread in Bearbeitung? Wahrscheinlich nicht. Lesen Sie Stephens Artikel darüber, warum es keinen Thread gibt.
  • Jetzt erwarten wir die Aufgabe. Was geschieht? Wir prüfen, ob die Aufgabe zwischen dem Zeitpunkt, an dem wir sie erstellt haben, abgeschlossen ist und wir darauf gewartet haben. Wenn ja, dann holen wir das Ergebnis und laufen weiter. Nehmen wir an, es ist nicht abgeschlossen. Wir unterschreiben den Rest dieser Methode als Fortsetzung dieser Aufgabe und Rückkehr .
  • Jetzt ist die Kontrolle zum Anrufer zurückgekehrt. Was tut es? Was immer es will.
  • Angenommen, die Aufgabe ist abgeschlossen. Wie hat es das gemacht? Vielleicht lief es in einem anderen Thread, oder vielleicht der Aufrufer, den wir gerade zurückgegeben haben, damit er im aktuellen Thread vollständig ausgeführt werden konnte. Unabhängig davon haben wir jetzt eine abgeschlossene Aufgabe.
  • Die abgeschlossene Aufgabe fragt den richtigen Thread – wiederum wahrscheinlich der einzige Thread – um die Fortsetzung der Aufgabe auszuführen.
  • Die Kontrolle geht sofort wieder in die Methode über, die wir gerade an der Stelle des Wartens verlassen haben. Jetzt steht ein Ergebnis zur Verfügung, damit wir text zuweisen und den Rest der Methode ausführen können.

Es ist wie in meiner Analogie. Jemand bittet dich um ein Dokument. Sie verschicken das Dokument per Post und machen weiter. Wenn es in der Post ankommt, wird Ihnen signalisiert, und wenn Ihnen danach ist, erledigen Sie den Rest des Workflows – öffnen Sie den Umschlag, bezahlen Sie die Liefergebühren, was auch immer. Sie müssen keinen anderen Arbeiter einstellen, um das alles für Sie zu tun.

In-Browser-Javascript ist ein großartiges Beispiel für ein asynchrones Programm, das keine Threads hat.

Sie müssen sich keine Sorgen machen, dass mehrere Teile des Codes die gleichen Objekte gleichzeitig berühren: Jede function wird beendet, bevor ein anderes Javascript auf der Seite ausgeführt werden darf.

Wenn jedoch etwas wie eine AJAX-Anforderung ausgeführt wird, wird überhaupt kein Code ausgeführt, sodass anderes JavaScript auf Ereignisse wie Klickereignisse reagieren kann, bis diese Anforderung zurückkommt und den zugeordneten callback aufruft. Wenn einer dieser anderen Ereignishandler noch ausgeführt wird, wenn die AJAX-Anforderung zurückkommt, wird sein Handler erst aufgerufen, wenn sie abgeschlossen sind. Es wird nur ein JavaScript-Thread ausgeführt, obwohl es möglich ist, dass Sie das, was Sie getan haben, effektiv unterbrechen können, bis Sie die Informationen haben, die Sie benötigen.

In C # -Anwendungen passiert das Gleiche immer dann, wenn Sie mit UI-Elementen arbeiten – Sie dürfen nur mit UI-Elementen interagieren, wenn Sie sich im UI-Thread befinden. Wenn der Benutzer auf eine Schaltfläche klickt und eine große Datei von der Festplatte lesen möchte, kann ein unerfahrener Programmierer den Fehler machen, die Datei im Click-Ereignishandler selbst zu lesen, wodurch die Anwendung erst “eingefroren” wird Das Laden der Datei wurde beendet, da es nicht mehr möglich ist, auf weitere klickende, schwebende oder andere UI-bezogene Ereignisse zu reagieren, bis dieser Thread freigegeben wird.

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 den Code dieses Threads anzugeben, dass der verbleibende Code im UI-Thread erneut ausgeführt werden muss, damit die UI-Elemente aktualisiert werden können basierend auf dem, was es in der Datei gefunden hat. Bis vor kurzem war dieser Ansatz sehr populär, weil er den C # -Bibliotheken und der Sprache leicht gemacht wurde, aber er ist wesentlich komplizierter als er sein muss.

Wenn Sie darüber nachdenken, was die CPU tut, wenn sie eine Datei auf der Hardware- / Betriebssystemebene liest, gibt sie im Grunde eine statement aus, Daten von der Platte in den Speicher zu lesen und das Betriebssystem mit einem “Interrupt” zu treffen Das Lesen ist abgeschlossen. Mit anderen Worten, das Lesen von der Festplatte (oder einer beliebigen E / A) ist eine inhärent asynchrone Operation. Das Konzept eines Threads, der darauf wartet, dass I / O abgeschlossen wird, ist eine Abstraktion, die die Bibliotheksentwickler erstellt haben, um das Programmieren einfacher zu machen. Es ist nicht nötig.

Jetzt haben die meisten E / A-Operationen in .NET eine entsprechende ...Async() -Methode, die Sie aufrufen können, die fast sofort eine Task zurückgibt. Sie können Callbacks zu diesem Task hinzufügen, um Code anzugeben, der beim Abschluss des asynchronen Vorgangs ausgeführt werden soll. Sie können auch angeben, in welchem ​​Thread dieser Code ausgeführt werden soll. Außerdem können Sie ein Token angeben, das der asynchrone Vorgang von Zeit zu Zeit überprüfen kann, um festzustellen, ob Sie die asynchrone Task abgebrochen haben und anmutig.

Bis die async/await Schlüsselwörter hinzugefügt wurden, war C # viel offensichtlicher, wie Callback-Code aufgerufen wird, da diese Callbacks in Form von Delegaten waren, die Sie mit der Aufgabe verknüpften. Um Ihnen weiterhin den Vorteil zu bieten, die ...Async() Operation zu verwenden und dabei Komplexität im Code zu vermeiden, werden die Delegierten durch async/await abstrahiert. Aber sie sind immer noch da im kompilierten Code.

Sie können also Ihren UI-Event-Handler await eine E / A-Operation await , den UI-Thread freigeben und mehr oder weniger automatisch zum UI-Thread zurückkehren, nachdem Sie die Datei gelesen haben – ohne jemals zu haben um einen neuen Thread zu erstellen.