Async, Await und Task

    • VB.NET
    • .NET (FX) 4.5–4.8

    Es gibt 6 Antworten in diesem Thema. Der letzte Beitrag () ist von Trade.

      Async, Await und Task

      Mit VS2012 haben 2 neue Schlüsselworte Einzug gehalten, der Async-Modifizierer, und der Await-Operator - teilweise recht gute Erklärungen findet man hier.

      Mit Async "modifiziert" man eine Methode, und die verhält sich dann im Zusammenspiel mittm Await-Operator sehr eigenartig:
      Zunächst ist sie ganz normal, aber beim Await returnt sie auf einmal - völlig verfrüht 8| !
      Der Await-Operator arbeitet währenddessen seinen Task ab, und der noch ausstehende Rest der Methode wird als "Continuation" registriert, also als Code, der im Original-Thread ausgeführt wird, nachdem der Await-Task erledigt ist.
      Das ist (für mich zumindest) bisserl schwer zu verstehen, dass eine Async-Methode in 2 Hälften gespalten ist, und als Axt fungiert der Await-Aufruf einer weiteren Methode, in der dann die Nebenläufigkeit stattfindet. Also wir haben:
      1. (Main-Thread) - erste Hälfte der Methode
      2. (Neben-Thread) - die Await aufgerufene Methode
      3. (Main-Thread) die Continuation, die 2. Hälfte der Methode
      Es ist allerdings ebenso praktisch, wie es verrückt klingt (halbe Methode - tss!). Denn das war immer das Problem beim Threading, dass man im MainThread Vorbereitungen treffen musste, dann den Thread abfahren, und dann die Nachbereitungen wieder im MainThread. Und das ist jetzt so einfach, dass mans fast gar nicht mehr merkt:

      VB.NET-Quellcode

      1. Private client As New HttpClient
      2. Private Sub btCancel_Click(sender As Object, e As EventArgs) Handles btCancel.Click
      3. client.CancelPendingRequests()
      4. End Sub
      5. Private Async Sub btGetHtml_Click(sender As Object, e As EventArgs) Handles btGetHtml.Click
      6. btCancel.Visible = True 'Vorbereitung
      7. Try
      8. txtResults.Text = Await client.GetStringAsync("http://msdn.microsoft.com")
      9. Catch ex As OperationCanceledException
      10. txtResults.Text = "Download aborted." 'Cancel-Nachbereitung
      11. End Try
      12. btCancel.Visible = False 'allg. Nachbereitung
      13. End Sub
      Eiglich unscheinbar, oder?
      Höchst erstaunlich aber, dass die Nachbereitung erst ca. 2 Sekunden nach Aufruf der btGetHtml_Click()-Methode stattfindet! Und trotzdem ist die Methode sofort zurückgekehrt, nach Sichtbarmachen von btCancel (zeile#8), denn sonst könnte man den ja gar nicht betätigen 8|
      Wie gesagt: Es ist ein Compiler-Trick, der den Nachbereitungs-Code als "Continuation" behandelt und an den Await-Task anhängt.
      Dadurch kehrt die Methode sofort nach der "Vorbereitung" zurückt, und die Nachbereitung findet in Wirklichkeit in einer Art Callback statt, wenn der Await-Task beendet.

      Vorraussetzungen
      1. Eine Async-modifizierte Methode sollte mindestens einen Await-Aufruf aufweisen, sonst hat der Modifizierer keinen Sinn.
      2. Await kann nur auf Task oder Task(Of T) angewendet werden. Üblicherweise wendet man es auf Methoden-Aufrufe an, die solch zurückgeben, aber man kann auch eine Variable awaiten, die einen Task enthält.
      3. Der Task muss bereits laufen - Await startet den nicht.
      4. Bei Task(Of T) ruft der Await-Operator dann gleich das Task-Result ab (Datentyp T).
        (Also client.GetStringAsync(url) (Zeile #10) returnt nicht String - wies aussieht - sondern Task(of String).)
      Unterstützung
      • Threading betrifft vorwiegend bestimmte Bereiche, wie Dateizugriffe, Internet-Zugriffe, Media-Verarbeitung. Die entsprechenden Klassen bieten daher abgestimmte Async-Methoden an (wie etwa hier: HttpClient.GetStringAsync())
      • Die Task-Klasse bietet einige Methoden, um Tasks zu erstellen und zu verarbeiten, etwa Task.Run(delegate), Task.WhenAny(taskList), Task.WhenAll(taskList), Task.Delay(milliseconds)
      • nicht zu vergessen System.Threading.CancellationTokenSource und System.Threading.CancellationToken
        Die bilden einen Standard zum Canceln und für Timeouts
      Ein paar Übungen
      1) selbstgemachtes Awaitable mit Task.Run()
      Hier eine Button-Animation. Controls soll man nicht animieren, aber zeigt schön das Canceln, und wie einfach man eine normale Methode nach Async "übersetzen" kann:

      VB.NET-Quellcode

      1. Private _Cts As CancellationTokenSource
      2. Private WithEvents _Progress As New Progress(Of Point)
      3. Private Sub btCancel_Click(sender As Object, e As EventArgs) Handles btCancel.Click
      4. _Cts.Cancel()
      5. End Sub
      6. Private Async Sub btMoveMe_Click(sender As Object, e As EventArgs) Handles btMoveMe.Click
      7. btCancel.Visible = True
      8. ProgressBar1.Visible = True
      9. ProgressBar1.Value = 0
      10. Dim pt = btMoveMe.Location
      11. _Cts = New CancellationTokenSource
      12. 'Dim duration = MoveControl(btMoveMe, ProgressBar1.Maximum, 1, _Cts.Token) 'so wäre normaler Aufruf
      13. Dim duration = Await Task.Run(Function() MoveControl(btMoveMe, ProgressBar1.Maximum, 1, _Cts.Token))
      14. _Cts.Dispose()
      15. MessageBox.Show(duration.ToString)
      16. btMoveMe.Location = pt
      17. btCancel.Visible = False
      18. ProgressBar1.Visible = False
      19. End Sub
      20. Private Function MoveControl(ctl As Control, count As Integer, stp As Integer, ct As CancellationToken) As Long
      21. Dim sw = Stopwatch.StartNew
      22. Dim pt = ctl.Location
      23. For i As Integer = 0 To count - 1
      24. pt.Offset(stp, stp)
      25. _Progress.Report(pt)
      26. Threading.Thread.Sleep(20)
      27. If ct.IsCancellationRequested Then Exit For
      28. Next
      29. Return sw.ElapsedMilliseconds
      30. End Function
      31. Private Sub _Progress_ProgressChanged(sender As Object, e As Point) Handles _Progress.ProgressChanged
      32. btMoveMe.Location = e
      33. ProgressBar1.Increment(1)
      34. End Sub
      Die Methode MoveControl() ist ganz normal gecodet, und dann wird sie in eine Awaitable Methode "eingepackt", nämlich in
      Task.Run(Function() MoveControl(btMoveMe, ProgressBar1.Maximum, 1, _Cts.Token)) (zeile #15).
      Genau genommen ist sie doppelt eingepackt, nämlich in eine anonyme Function, die sie aufruft, und diese anonyme Function wiederum wird an Task.Run() übergeben, und Task.Run() startet sie im NebenThread und returnt einen Task(Of Long) - ist also Awaitable.
      Vor- / Nach-bereitung (zeilen #9-11 / #19-21) passen das Gui an, und MoveControl() handelt sowohl Cancellation als auch die Progressbar.
      Was ausserdem zu sehen ist, dass man nicht mehr mit Control.Invoke() arbeiten muss, sondern heutzutage gibt es eine Klasse Progress(Of T), der man im NebenThread Änderungen mitteilen kann (#28), und die feuert dann im MainThread ein Event, wo sie die Änderung weitergibt (#35-38).

      2) Async durchreichen ( + Timeout)
      Eine Eigenschaft Async-modifizierter Function hab ich bisher unterschlagen: sie returnen selbst Task oder Task(Of T), sind also selbst Awaitable.
      Das wird hier genutzt, um das etwas kompliziertere Durchnudeln einer Reihe von Web-Zugriffen in eine Extra-Methode auszulagern.
      Denn der ButtonClick-Handler ist mit Vorbereitung, Timeout/Cancel-Handling und Nachbereitung komplex genug.

      VB.NET-Quellcode

      1. Private _Urls() As String = {
      2. "http://msdn.microsoft.com",
      3. "http://msdn.microsoft.com/en-us/library/hh290138.aspx",
      4. "http://msdn.microsoft.com/en-us/library/hh290140.aspx",
      5. "http://msdn.microsoft.com/en-us/library/dd470362.aspx",
      6. "http://msdn.microsoft.com/en-us/library/aa578028.aspx",
      7. "http://msdn.microsoft.com/en-us/library/ms404677.aspx",
      8. "http://msdn.microsoft.com/en-us/library/ff730837.aspx"
      9. }
      10. Dim _Cts As CancellationTokenSource
      11. Private Async Sub startButton_Click(sender As Object, e As EventArgs) Handles startButton.Click
      12. _Cts = New CancellationTokenSource()
      13. txtResults.Clear()
      14. btCancel.Show()
      15. Dim sw = Stopwatch.StartNew
      16. Try
      17. ' ***Set up the CancellationTokenSource to cancel after 2.5 seconds.
      18. _Cts.CancelAfter(2500)
      19. Await AccessTheWebAsync(_Cts.Token)
      20. txtResults.AddLine("Downloads complete.")
      21. Catch ex As OperationCanceledException
      22. txtResults.AddLine("Downloads canceled.")
      23. End Try
      24. txtResults.AddLine(sw.ElapsedMilliseconds, " Milliseconds")
      25. _Cts.Dispose()
      26. btCancel.Hide()
      27. End Sub
      28. Async Function AccessTheWebAsync(ct As CancellationToken) As Task
      29. Dim client As HttpClient = New HttpClient()
      30. For Each url In _Urls
      31. Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)
      32. Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
      33. txtResults.AddLine("Length of the downloaded string: ", urlContents.Length)
      34. Next
      35. End Function


      3) Task.WhenAny() - den schnellsten von mehreren Tasks auswerten

      VB.NET-Quellcode

      1. Private Async Sub bt_Click(sender As Object, e As EventArgs) Handles btStart.Click
      2. txtResults.Clear()
      3. Dim client = New HttpClient()
      4. Dim sw = Stopwatch.StartNew
      5. Try
      6. Dim Tasks = Array.ConvertAll(_Urls, Function(url) ProcessURLAsync(url, client))
      7. Dim tskFirst = Await Task.WhenAny(Tasks)
      8. client.CancelPendingRequests()
      9. txtResults.AddLine("Length of the downloaded web site: ", Await tskFirst)
      10. txtResults.AddLine("Download complete.")
      11. Catch ex As OperationCanceledException
      12. txtResults.AddLine("Download canceled.")
      13. End Try
      14. txtResults.AddLine(sw.ElapsedMilliseconds, " Milliseconds")
      15. End Sub
      16. Async Function ProcessURLAsync(url As String, client As HttpClient) As Task(Of Integer)
      17. Console.WriteLine(url)
      18. Dim response As HttpResponseMessage = Await client.GetAsync(url)
      19. Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
      20. Return urlContents.Length
      21. End Function
      3a) Task.WhenAny() returnt einen Task(Of Task(Of Integer)), und das erste Await (#7) ruft daher den schnellsten Task(Of Integer) ab, und davon wiederum ruft das 2. Await die Download-Size ab (#9).
      3b) Beachte, dass im 2. Await eine Variable awaitet wird, und der enthaltene Task ist bereits durchgelaufen, also wirklich gewaitet wird da nicht.
      Hmm - also das 2.Await nicht wirklich sinnvoll, wie's da steht - wer mag, ersetze also Zeile #9 mittxtResults.AddLine("Length of the downloaded web site: ", tskFirst.Result). Weil ist besserer Stil, unnützes nicht zu benützen :saint:
      3c) Vergleiche auch die ProcessURLAsync()-Deklaration As Task(Of Integer) mit ihrer Return-Anweisung in zeile #21: In einer normalen Function wäre es syntax-widrig, einen Integer zu returnen, wenn die Function nicht entsprechend als Integer deklariert ist.
      Naja - in einer Async-Function ist das eben nicht syntaxwidrig, sondern muss so :P

      4) Tasks sofort auswerten, wenn sie eintrudeln
      Übung 2 ist ineffizient, denn es wird eine url nach der anneren abgerufen. Übung 3 ist verschwenderisch, denn alle Tasks nach dem ersten werden gecancelt.
      Hier also eine Schleife (#4-8), die immer den schnellsten auswertet und dann aus der Task-Liste entfernt.

      VB.NET-Quellcode

      1. Async Function AccessTheWebAsync(ct As CancellationToken) As Task
      2. Dim client As HttpClient = New HttpClient()
      3. Dim Tasks = _Urls.Select(Function(url) ProcessURLAsync(url, client, _Cts.Token)).ToList
      4. While Tasks.Count > 0
      5. Dim tskFirst As Task(Of Integer) = Await Task.WhenAny(Tasks)
      6. Tasks.Remove(tskFirst)
      7. txtResults.AddLine("Length of download: ", tskFirst.Result)
      8. End While
      9. End Function
      10. Async Function ProcessURLAsync(url As String, client As HttpClient, ct As CancellationToken) As Task(Of Integer)
      11. Console.WriteLine(url)
      12. Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)
      13. Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
      14. Return urlContents.Length
      15. End Function
      Beachte auch, dass hier nicht wie zuvor mit HttpClient.CancelPendingRequests() gecancelt wird, sondern zur Abwechslung mal wieder per CancellationToken (vgl. auch Übung 1)
      Dateien
      • AsyncTester.zip

        (35,25 kB, 585 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 12 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Ansichts- oder Definitions-Sache: Ich rede halt von Threading, und folglich von Threads, weil ich sonst von "Tasking" reden müsste - oder welche Wortwahl schlägst du vor?
      Und nachwievor ist es richtig, von einem Main-Thread zu reden - oder ist das auf einmal kein Thread mehr? Und wenn es einen Main-Thread gibt, dann wirds wohl auch noch Neben-Threads geben - egal wie man die nun nennt - ich nenne sie halt Neben-Threads.
      Und nachwievor bekommt man ja auch eine Exception von wegen "unzulässiger threadübergreifender Aufruf", wenn man Threading (oder wie man's nun nennt) unsachgemäß anwendet.

      Ich hab jetzt auch auf Wiki nachgeguckt, am ehesten deckt sich die hiesige Verwendung des Begriffes wohl mit dem, was dort als "User-Thread" beschrieben wird: de.wikipedia.org/wiki/User-Thread

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Threading kann man hier mmn schon sagen. Es stimmt, Task und Thread sind ersteinmal zwei verschiedene Bergriffe (ein Task ist einfach nur eine Aufgabe, die ausgeführt und auf die gewartet werden kann, Parallelisierung ist hier zunächst nicht mitinbegriffen), jedoch geht es in diesem Tutorial ja ausschließlich um die Nutzung von Tasks mit async + await, also die Parallelisierung mittels Tasks, und diese findet im Hintergrund immer mit Threads (hier Threadpool) statt, egal wie die Wrapperklasse heißt.
      Wenn man pingelig sein möchte könnte man anstelle von Multithreading von Parallelisierung oder Asynchronität sprechen, diese Begriffe sind in der Praxis aber nahezu synonym (und Programmieren ist ja praxisorientiert).

      ErfinderDesRades schrieb:

      Tasks sofort auswerten, wenn sie eintrudeln
      Also bei der 4. Methode wird mir beim Abbrechen eine AggregateException in Zeile #22 geworfen. Siehe Bild...

      Daher habe ich bei Zeile #37

      VB.NET-Quellcode

      1. If _Cts.Token.IsCancellationRequested Then Throw New OperationCanceledException("A task was canceled.")
      eingefügt, um den Abbruch im TryCatch-Zweig richtig abzufangen.

      Meine Frage:
      Hat sich da etwa etwas im Framework geändert und ist diese Vorgehensweise nun die Richtige?

      Lg
      VB1963
      Tja, da habich wohl nicht gründlich genug getestet.
      Ich hatte einfach angenommen, dass die Exception fliegt, wenn das Token im Aufruf mitgegeben wird - auch mittelbar. Ist wohl nicht so.
      Aber man würde wohl keine eigene OperationCancelledException werfen, sondern das CancellationToken das selbst machen lassen:

      VB.NET-Quellcode

      1. ct.ThrowIfCancellationRequested()
      Außer man muss natürlich noch andere Aktionen ausführen, bevor die Exception dann fliegt. ;)
      Was ich noch (zu Übung 1) sagen möchte: Es wird sehr oft der Fehler gemacht, dass man was doppelt moppelt.

      Nehmen wir Deinen Code:

      VB.NET-Quellcode

      1. Async Function AccessTheWebAsync(ct As CancellationToken) As Task
      2. Dim client As HttpClient = New HttpClient()
      3. For Each url In _Urls
      4. Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)
      5. Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
      6. txtResults.AddLine("Length of the downloaded string: ", urlContents.Length)
      7. Next
      8. End Function


      Die Methode verläuft ohne inneren Await-Aufruf synchron und mit eben asynchron (Das ist hier der Fall). Gerne passiert es mal am Anfang, dass man das nochmal in ein Task.Run wrappt und dann kommt es irgendwo zu 'nem Deadlock. Also wirklich nur für Methoden verwenden, die normal synchron laufen und dann in einen asynchronen Task gewrappt werden sollen.
      Und man kann async-await auch in .NET 4.0 nutzen, nicht nur .NET 4.5. Dazu einfach via NuGet "Microsoft Async" installieren und man kann das nachreichen. Entsprechende Methoden wie Task.Run werden dann mit einer speziellen TaskEx-Klasse hinzufügt/nachgereicht. Das wäre dann also TaskEx.Run.

      ErfinderDesRades schrieb:

      Ich hatte einfach angenommen, dass die Exception fliegt, wenn das Token im Aufruf mitgegeben wird
      Nope, das muss eben bei eigenen Tasks immer manuell ausgelöst werden. Nur bei Task.Wait z. B. wird das intern automatisch geworfen, sobald man abbricht.

      Grüße
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Programmier-Fragen per PN, denn dafür ist das Forum da :!: