VB.NET Await – Asynchrone Programmierung made easy
Inhaltsverzeichnis
VB.NET Await
Du möchtest mehr über VB.NET Await lernen, verstehen und für die asynchrone Programmierung in deinem Programm verwenden?
Lerne in diesem Beitrag, wie Du die seit geraumer Zeit eingeführten Async/Await Schlüsselwörter verwenden kannst, um dein Programm und dessen Abläufe asynchron zu gestalten.
Gerne möchte ich Dich auch noch auf andere vielleicht für Dich interessante Beiträge hinweisen: Callbacks, AndAlso, BackgroundWorker Beispiel.
Da hängt doch alles!
Was im ersten Moment wie ein natürliches, menschliches Altersleiden klingt, kann man in der Programmierung weitestgehend vermeiden.
Wenn man sein Programm z. B. – aus welchen Gründen auch immer – zwischenzeitlich warten lassen möchte, kennt man den damaligen Aufwand nur zu gut.
Weitere Threads die „gespawnt“ und korrekt verarbeitet werden möchten, ein BackgroundWorker der wie im obigen angesprochenen Beispiel zum Einsatz kommt, uvm.
Es war in Vergangenheit teilweise ein relativer Graus diese Aufgabe mit einfachen Bordmitteln zu lösen.
Das wohl simpelste Beispiel
Für das folgende Beispiel brauchst Du nur ein kinderleichtes Programm mit einem einzigen Button.
Dieser Button ruft letztendlich nur 2-3 Anweisungen auf und schon können wir das Problemchen im seinem Umfang betrachten.
Natürlich kann der Code auch z. B. alternativ im Form Load-Ereignishandler platziert werden.
Code – VB.NET Await
Im jetzt kommenden Schritt erkläre ich Dir etwas mehr über die Details der Vorgehensweise im Code.
Ohne asynchrone Weise
Dazu starten wir im ersten Schritt mit der synchronen Weise, wie Du auch hier im folgenden, animierten Bild betrachten kannst.
Dabei klicke ich auf den Button, Welcher seine Arbeit macht und die 3 Anweisungen nacheinander ausführt.
Leider wird die „Sleep„-Anweisung der Thread-Klasse somit auf dem GUI-Thread ausgeführt, was das Programm leider zum Stottern bringt.
Dies passiert nachdem der Handler die erste „WriteLine“-Anweisung ausgeführt hat und danach im wahrsten Sinne des Wortes „schlafen“ geht.
Ich weiß ja nicht wie es Dir geht, aber mit mir kann eigentlich keiner reden, während ich schlafe – so ist es auch hier!
Gehe für den nächsten Schritt in den Klick-Ereignishandler des Buttons und schaue Dir folgenden Code an:
Private Sub btnAwait_Click(sender As Object, e As EventArgs) Handles btnAwait.Click Debug.WriteLine("Before") Thread.Sleep(3000) Debug.WriteLine("After") End Sub
Um den kommenden Effekt besonders verstehen und „fühlen“ zu können, kannst Du den Button nun einmal testweise anklicken.
Versuche dann einmal die Form zu bewegen, jedoch wirst Du feststellen, dass da nichts mehr reagiert.
Obwohl ich also eine ganz normale – ich sage mal – „Drag„-Anweisung mit der Maus auf dem Form-Rand ausführe, passiert gar nichts.
Das liegt daran, dass wir den „Main„, bzw. den GUI-Thread (die grafische Oberfläche) – im wahrsten Sinne des Wortes – schlafen legen.
Frust kommt auf – User Experience (UX)
Allerdings ist das freilich nicht das gewünschte Verhalten – in den meisten Fällen zumindest.
Wer möchte schon auf einen Button klicken und sich Fragen „passiert da jetzt was?“?
Der typische ungeduldige Nutzer wird verständlicherweise noch einmal klicken und wieder kommt nichts dabei rum – seufz..
Da freut sich dann die Datenbank besonders, wenn Sie statt einem Vorgang, durch schlechte Programmierung und einem ungeduldigen Nutzer, das 5-fache an Last verarbeiten muss.
Ich selbst kann mich mehr als zu gut an viele Programme erinnern, wo ich ähnlich blöd vor dem Bildschirm aus der Wäsche geschaut habe.
Besonders Datenbank-, oder anderweitige Netzwerk–Zugriffe haben mich – vor allem je nach Datenmenge – an die Gedulds-Grenzen gebracht.
Diese damaligen typisch grauen Oberflächen, worin man sekunden-, bzw. minutenlang auf eine Antwort seitens des Programms warten durften – bäh.
Jetzt asynchron – mit der VB.NET Await Anweisung
Nun stellen wir das Beispiel mit den tollen modernen Mitteln, also mit „VB.NET Await“, bzw. „VB.NET Async Await“ – verfügbar seit Visual Studio 2012 – um.
Das können wir in diesem Fall mit 2 wirklich sehr einfachen Änderungen erreichen!
Im ersten Schritt markieren wir die Methode mit dem Async-Modifizierer als asynchron.
Danach ersetzen wir die „Thread Sleep“ Warte-Funktion durch das passende Äquivalent aus der Task-Klasse namens Delay.
Private Async Sub btnAwait_Click(sender As Object, e As EventArgs) Handles btnAwait.Click Debug.WriteLine("Before") Await Task.Delay(3000) Debug.WriteLine("After") End Sub
Der letzte hinzugekommene Teil, ist der Await-Operator, Welcher sehr einfach gesagt auf den Abschluss der Aufgabe wartet, Welche von Delay zurückgegeben wird.
Nun hängt nichts mehr!
Auch ohne Anti Aging Produkte – kleiner Spaß beiseite – hängt nun nichts mehr.
Probiere dies gerne sofort aus und klicke den Button nach den vorgenommenen Änderungen erneut an.
Nun wird der Hover–Effekt des Buttons weiterhin sichtbar und du wirst bei ausreichendem Delay-Interval auch in der Lage sein die Form zu bewegen.
Async Await ist nicht immer so einfach
Nun sind wir auch letztendlich bei einem wichtigen Punkt, wir können leider nicht immer so easy verfahren.
Passende Gegebenheiten
Um Async/Await verwenden zu können, müssen wir auch passende Gegebenheiten vorfinden, oder eben erstellen.
Du kannst nicht einfach jede beliebige Methode als Async markieren und hoffen, dass das immer so funktioniert – leider..
Um mit Hilfe des Await-Operators auf den Abschluss einer Aufgabe zu warten, müssen wir eine Aufgabe vorfinden.
Einige Funktionen des NET Frameworks geben dir eine Aufgabe als Rückgabewert einer Funktion zurück.
Darunter fällt auch die oben gezeigte „Task.Delay“-Funktion.
Weitere Beispiele wären hier z. B. diverse Funktionen wie DownloadFileAsync, oder DownloadStringAsync aus der WebClient-Klasse.
Gegebenheiten schaffen
Wenn wir einmal nicht solche tollen Gegebenheiten vorfinden, wir also keine „hausgemachte“ Aufgabe vorfinden, müssen wir selbst ran.
Aber auch das ist mit den modernen NET Framework Mitteln nicht wirklich schwierig, vorausgesetzt man weiß wie!
Das lassen wir an dieser Stelle erstmal noch ein wenig hinten an stehen, da wir uns erstmal mit weiteren Basics beschäftigen sollte.
Linearer Verlauf – The usual business
Bevor wir uns allerdings mit dem „Erstellen“ eigener Tasks beschäftigen, schauen wir uns noch ein kleines Basis-Konzept an.
Häufig verwechseln Anfänger diverse Situationen, bzw. verschiedene Gegebenheiten, daher möchte ich hier einmal kurz 2 Basics erklären.
Für gewöhnlich wird der von uns geschriebene Code schön Zeile für Zeile, also linear ausgeführt.
Stelle Dir vor, Du hast 3 Anweisungen in einer Methode namens „DoStatements“ definiert:
Private Sub DoStatements() Statement1() Statement2() Statement3() End Sub
Wenn Du dann die Methode „DoStatements“ aufrufst, werden die darin befindlichen Statements (1, 2, 3) nacheinander ausgeführt.
Um den Kontrast gleich noch stärker hervorzuheben, rufe die Methode „DoStatements“ z. B. 3x auf.
Das Aufrufen der Methode „DoStatements“ ist natürlich selbst auch eine Anweisung – just sayin‘.
Jeder einzelne Aufruf der übergeordneten Methode (DoStatements), ruft selbst wiederum die untergeordneten Statements 1-3 auf.
Der Ablauf wäre also:
- DoStatements
- Statement 1
- Statement 2
- Statement 3
- DoStatements
- Statement 1
..und so weiter.
Fire and Forget
Nun kommen wir im Bezug zum „Async Await“-Pattern zu einer kleinen Änderung des obigen Codes und schauen, wie sich das Ganze dann verhält.
Kennzeichne die Methode „DoStatements“ nun als Async, indem Du das dafür zuständige Schlüsselwort anhängst:
Statt einer „privaten Methode„, ist es nun eine „private, asynchrone Methode„:
Private Async Sub DoStatements() Statement1() Statement2() Statement3() End Sub
Leider passiert dadurch im ersten Moment nicht viel, außer das uns eine neue Fehlermeldung an den Kopf geworfen wird.
Wir werden gewarnt, dass wir zwar sagen: „Hey die Methode soll asynchron ausgeführt werden“, aber letztendlich keine Await-Operationen darin ausführen.
Basically sagt uns das: „Warum soll die Methode bitte async sein, wenn Du dort keinen async Kram machst!?“.
Damit ich das „Fire & Forget“ besser für Dich hier darstellen kann, folgen wir entweder dem Vorschlag der IDE, oder verwenden einen kleinen Trick.
Workaround für die Demo
Rufe in der ersten Zeile der „DoStatements“-Methode einfach mal:
Await Task.CompletedTask
auf, also so ungefähr (oder irgendwo anders in der Sub):
Private Async Sub DoStatements() Await Task.CompletedTask Statement1() Statement2() Statement3() End Sub
Somit können wir nun auf eine abgeschlossene Task warten, um der „Async Sub“ vorzugaukeln, hier würde asynchrone Arbeit passieren.
Beachte bitte, dass dieser Code nur der Demonstration hier dient!
Nun ist die obige Fehlermeldung verschwunden und die Sub wird nun asynchron ausgeführt.
Nun gehts a(b)synchron
Wenn Du nun die „DoStatements„-Methode 3x aufrufst, wird Diese nicht wie vorher ausgeführt.
Schaue Dir dazu am besten dieses Bild an:
Vorher haben die Schritte 4 und 5 – also weitere Aufrufe der „DoStatements“-Methode – auf die Beendigung der vorherigen Aufrufe gewartet.
Dies sieht nun anders aus, da die Schritte 1-3 hintereinander durchgefeuert werde.
Dadurch laufen Sie im Hintergrund nun mehr oder weniger gleichzeitig.
Einsatzmöglichkeiten von Fire & Forget
Denke für eine beispielhafte Einsatzmöglichkeit von unserer kleinen „Fire & Forget“-Variante von oben einfach mal an einen Chat-Server.
Ich selbst habe Ähnliches auch schon für eine Art Bestellungs–Server bei einer ehemaligen Anstellung verwendet.
Klar könnte man dafür auch einfach die GUI, bzw. den GUI-Thread vermeiden und nach Möglichkeit in einer Konsolenanwendung arbeiten.
Leider ist das aber nicht immer möglich und hängt natürlich stets von den Anforderungen des Kunden, etc. ab!
Ganz rudimentär könnte es ggf. so aussehen:
(Man könnte natürlich auch noch durch Enum-Werte verschiedene States wie „Starting, Stopping, Running, blabla“ abdecken..):
Public Class ChatServer Public Property IsRunning As Boolean Public Async Sub Start() CheckAndThrowIfAlreadyRunning() IsRunning = True RaiseEvent Started(Me, EventArgs.Empty) While IsRunning Await DoServerWork() End While RaiseEvent Stopped(Me, EventArgs.Empty) End Sub Private Function DoServerWork() As Task ' simulate some work.. Return Task.Delay(200) End Function Public Sub [Stop]() CheckAndThrowIfNotRunning() End Sub Private Sub CheckAndThrowIfAlreadyRunning() If IsRunning Then Throw New InvalidOperationException("The ChatServer is already running") End If End Sub Private Sub CheckAndThrowIfNotRunning() If Not IsRunning Then Throw New InvalidOperationException("The ChatServer isn't running") End If End Sub Public Event Started As EventHandler Public Event Stopped As EventHandler End Class
Wenn Du nun z. B. noch einen Email-Server, oder weitere ähnliche Server erstellst, müssen Diese natürlich irgendwie zeitgleich laufen.
Rufen wir die rudimentären Beispiele der Server einfach mal in einer Forms-Anwendung auf, bzw. starten Diese:
Public Class Form1 Private _chatServer As ChatServer ' Private _emailServer As EmailServer ' Private _orderServer As OrderServer Sub New() InitializeComponent() _chatServer = New ChatServer() ' _emailServer = New EmailServer() ' _orderServer = New OrderServer() End Sub Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load StartAllServers() End Sub Private Sub StartAllServers() ' fires off all async, not waiting for a single "start" to "finish" _chatServer.Start() ' _emailServer.Start() ' _orderServer.Start() End Sub End Class
Wie im Kommentar angemerkt würde die „StartAllServers“-Methode alle Start-Anweisungen nacheinander losfeuern.
Dabei wartet Sie auf keinen einzigen Abschluss der Einzelnen Methoden – fire & forget halt.
Man könnte nun natürlich noch die einzelnen Ereignisse abonnieren und Weitere hinzufügen.
Somit könntest Du eine „losgelöste“ Kommunikation zwischen der GUI und ggf. auch den einzelnen „Server-Modulen“ realisieren.
Weiteres folgt
Dieser Beitrag wird bald noch einmal erweitert – also stay tuned :D!