VB.NET Await – Asynchrone Programmierung made easy

VB.NET Await
VB.NET Await

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.

VB.NET Await Beispiel GUI
VB.NET Await Beispiel GUI

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 „schlafengeht.

Ich weiß ja nicht wie es Dir geht, aber mit mir kann eigentlich keiner reden, während ich schlafe – so ist es auch hier!

VB.NET Form hängt bei Sleep
VB.NET Form hängt bei Sleep

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 undfü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 NetzwerkZugriffe 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!

VB.NET Form hängt nicht mehr bei Sleep
VB.NET Form hängt nicht mehr bei Sleep

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 HoverEffekt 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.

Async Await nicht immer so einfach
Async Await nicht immer so einfach

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
Linearer Code, bzw. einzelne Anweisungen in einer Methode (Sub)
Linearer Code, bzw. einzelne Anweisungen in einer Methode (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‘.

3facher, linearer Aufruf der DoStatements Methode
3facher, linearer Aufruf der DoStatements Methode

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!?“.

Async DoStatements Methode Fehler - Es fehlen Await Operatoren
Async DoStatements Methode Fehler – Es fehlen Await Operatoren

Damit ich das „Fire & Forgetbesser 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 synchron – nachher asynchron
Vorher synchron – nachher asynchron

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 BestellungsServer 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!

Downloads

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.