Nebenläufigkeit, Parallelität und Async

Problem

Deine Anwendung benötigt ein Kundenprofil und aktuelle Bestellungen von zwei unabhängigen Diensten. Sie wartet auf das Profil, bevor sie die Bestellungen anfordert. Du möchtest die Arbeit zeitlich überlappen lassen und verstehen, wie sich das von der Nutzung mehrerer CPU-Kerne unterscheidet.

Lösung — Nebenläufige, parallele und asynchrone Ausführung

Nebenläufigkeit: Aufgaben überlappen sich

Nebenläufigkeit bedeutet, dass mehrere Aufgaben in überlappenden Zeiträumen vorankommen. Ein einzelner Thread kann zwischen Aufgaben wechseln oder eine andere Aufgabe bearbeiten, während eine auf ein Ergebnis wartet.

Parallelität: Arbeit läuft gleichzeitig

Parallelität bedeutet, dass Arbeit tatsächlich gleichzeitig ausgeführt wird, beispielsweise auf verschiedenen CPU-Kernen. Unabhängige Berechnungen können dadurch schneller werden, wenn der Nutzen den Koordinationsaufwand übersteigt.

Async: Die Methode pausiert beim Warten

Asynchrone Ausführung erlaubt es einer Operation, die Kontrolle zurückzugeben, bevor ihr Ergebnis vorliegt. Der Aufrufer kann andere Arbeit erledigen und später auf den Abschluss reagieren. Mit asynchroner Ein-/Ausgabe kann eine Anwendung beide Anfragen starten und auf ihre Ergebnisse warten, ohne für jede Anfrage einen eigenen Thread zu blockieren. Der genaue Mechanismus hängt von der Laufzeitumgebung und der API ab.

Was passiert mit dem Thread? Ein Beispiel aus C#/.NET

Das Schlüsselwort async kennzeichnet eine Methode, die await verwenden kann. Eine mögliche Pause entsteht bei await, nicht beim Schlüsselwort async. Stell dir eine Methode vor, die eine Anfrage vorbereitet, auf eine HTTP-Antwort wartet und anschließend das Ergebnis verarbeitet. Schritt 1 — Den Anfang ausführen. Beim Aufruf startet diese Methode auf dem aufrufenden Thread. Dieser Thread führt die Vorbereitung aus und startet die HTTP-Operation. Das Hinzufügen von async verschiebt diese Arbeit nicht automatisch auf einen anderen Thread. Schritt 2 — await erreichen. Ist die HTTP-Operation noch nicht abgeschlossen, pausiert die Methode. Ihre lokalen Zustandsdaten und die Stelle für die Fortsetzung bleiben erhalten. Die Kontrolle geht an den Aufrufer zurück, zusammen mit einem Task, der den späteren Abschluss der Methode repräsentiert. Schritt 3 — Andere Arbeit ermöglichen. Der Thread kann zunächst den Code des Aufrufers weiter ausführen. Sobald die Kontrolle an die Laufzeitumgebung oder Ereignisschleife zurückgeht, kann er andere Arbeit übernehmen, etwa eine weitere Anfrage oder ein Ereignis der Benutzeroberfläche. Bei echter asynchroner Ein-/Ausgabe muss kein eigener Thread blockiert auf diese HTTP-Antwort warten. Das ist mit einem „freigegebenen“ Thread gemeint: Die pausierte Methode belegt ihn während des Wartens nicht mehr. Der Thread existiert weiterhin, und der Aufrufer kann noch Arbeit auf ihm ausführen. Schritt 4 — Die Methode fortsetzen. Nach Abschluss der Operation wird die Ausführung am await fortgesetzt, um das Ergebnis abzurufen oder einen Fehler zu verarbeiten. Nach einem erfolgreichen await läuft der restliche Code wieder auf einem Thread. Je nach Ablaufplanung der Laufzeitumgebung und Kontext der Anwendung kann das der ursprüngliche oder ein anderer Thread sein.
Vor await → Vorbereitung auf dem aufrufenden Thread
Unfertige E/A bei await → Methode pausiert; zurück zum Aufrufer
Nach erfolgreichem await → mit dem Ergebnis weiterarbeiten
Ist die Operation bereits abgeschlossen, wenn C# das await erreicht, kann die Methode ohne Pause sofort fortfahren. CPU-Berechnungen vor oder nach await belegen weiterhin den Thread, der sie ausführt.
Kerngedanke: Nebenläufigkeit lässt Aufgaben zeitlich überlappen, Parallelität führt Arbeit gleichzeitig aus, und await kann eine Methode pausieren lassen, ohne ihren Thread während des Wartens zu blockieren.