Mit VS2012 haben 2 neue Schlüsselworte Einzug gehalten, der
Mit
Zunächst ist sie ganz normal, aber beim
Der
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:
Eiglich unscheinbar, oder?
Höchst erstaunlich aber, dass die Nachbereitung erst ca. 2 Sekunden nach Aufruf der
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) selbstgemachtes Awaitable mit
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:
Die Methode
Genau genommen ist sie doppelt eingepackt, nämlich in eine anonyme Function, die sie aufruft, und diese anonyme Function wiederum wird an
Vor- / Nach-bereitung (zeilen #9-11 / #19-21) passen das Gui an, und
Was ausserdem zu sehen ist, dass man nicht mehr mit Control.Invoke() arbeiten muss, sondern heutzutage gibt es eine Klasse
2) Async durchreichen ( + Timeout)
Eine Eigenschaft Async-modifizierter
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.
3) Task.WhenAny() - den schnellsten von mehreren Tasks auswerten
3a)
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 mit
3c) Vergleiche auch die
Naja - in einer Async-Function ist das eben nicht syntaxwidrig, sondern muss so
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.
Beachte auch, dass hier nicht wie zuvor mit
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 !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:
- (Main-Thread) - erste Hälfte der Methode
- (Neben-Thread) - die Await aufgerufene Methode
- (Main-Thread) die Continuation, die 2. Hälfte der Methode
VB.NET-Quellcode
- Private client As New HttpClient
- Private Sub btCancel_Click(sender As Object, e As EventArgs) Handles btCancel.Click
- client.CancelPendingRequests()
- End Sub
- Private Async Sub btGetHtml_Click(sender As Object, e As EventArgs) Handles btGetHtml.Click
- btCancel.Visible = True 'Vorbereitung
- Try
- txtResults.Text = Await client.GetStringAsync("http://msdn.microsoft.com")
- Catch ex As OperationCanceledException
- txtResults.Text = "Download aborted." 'Cancel-Nachbereitung
- End Try
- btCancel.Visible = False 'allg. Nachbereitung
- End Sub
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 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
- Eine
Async
-modifizierte Methode sollte mindestens einenAwait
-Aufruf aufweisen, sonst hat der Modifizierer keinen Sinn. Await
kann nur aufTask
oderTask(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.- Der Task muss bereits laufen -
Await
startet den nicht. - Bei
Task(Of T)
ruft derAwait
-Operator dann gleich das Task-Result ab (DatentypT
).
(Alsoclient.GetStringAsync(url)
(Zeile #10) returnt nicht String - wies aussieht - sondernTask(of String)
.)
- 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
undSystem.Threading.CancellationToken
Die bilden einen Standard zum Canceln und für Timeouts
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
- Private _Cts As CancellationTokenSource
- Private WithEvents _Progress As New Progress(Of Point)
- Private Sub btCancel_Click(sender As Object, e As EventArgs) Handles btCancel.Click
- _Cts.Cancel()
- End Sub
- Private Async Sub btMoveMe_Click(sender As Object, e As EventArgs) Handles btMoveMe.Click
- btCancel.Visible = True
- ProgressBar1.Visible = True
- ProgressBar1.Value = 0
- Dim pt = btMoveMe.Location
- _Cts = New CancellationTokenSource
- 'Dim duration = MoveControl(btMoveMe, ProgressBar1.Maximum, 1, _Cts.Token) 'so wäre normaler Aufruf
- Dim duration = Await Task.Run(Function() MoveControl(btMoveMe, ProgressBar1.Maximum, 1, _Cts.Token))
- _Cts.Dispose()
- MessageBox.Show(duration.ToString)
- btMoveMe.Location = pt
- btCancel.Visible = False
- ProgressBar1.Visible = False
- End Sub
- Private Function MoveControl(ctl As Control, count As Integer, stp As Integer, ct As CancellationToken) As Long
- Dim sw = Stopwatch.StartNew
- Dim pt = ctl.Location
- For i As Integer = 0 To count - 1
- pt.Offset(stp, stp)
- _Progress.Report(pt)
- Threading.Thread.Sleep(20)
- If ct.IsCancellationRequested Then Exit For
- Next
- Return sw.ElapsedMilliseconds
- End Function
- Private Sub _Progress_ProgressChanged(sender As Object, e As Point) Handles _Progress.ProgressChanged
- btMoveMe.Location = e
- ProgressBar1.Increment(1)
- End Sub
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
- Private _Urls() As String = {
- "http://msdn.microsoft.com",
- "http://msdn.microsoft.com/en-us/library/hh290138.aspx",
- "http://msdn.microsoft.com/en-us/library/hh290140.aspx",
- "http://msdn.microsoft.com/en-us/library/dd470362.aspx",
- "http://msdn.microsoft.com/en-us/library/aa578028.aspx",
- "http://msdn.microsoft.com/en-us/library/ms404677.aspx",
- "http://msdn.microsoft.com/en-us/library/ff730837.aspx"
- }
- Dim _Cts As CancellationTokenSource
- Private Async Sub startButton_Click(sender As Object, e As EventArgs) Handles startButton.Click
- _Cts = New CancellationTokenSource()
- txtResults.Clear()
- btCancel.Show()
- Dim sw = Stopwatch.StartNew
- Try
- ' ***Set up the CancellationTokenSource to cancel after 2.5 seconds.
- _Cts.CancelAfter(2500)
- Await AccessTheWebAsync(_Cts.Token)
- txtResults.AddLine("Downloads complete.")
- Catch ex As OperationCanceledException
- txtResults.AddLine("Downloads canceled.")
- End Try
- txtResults.AddLine(sw.ElapsedMilliseconds, " Milliseconds")
- _Cts.Dispose()
- btCancel.Hide()
- End Sub
- Async Function AccessTheWebAsync(ct As CancellationToken) As Task
- Dim client As HttpClient = New HttpClient()
- For Each url In _Urls
- Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)
- Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
- txtResults.AddLine("Length of the downloaded string: ", urlContents.Length)
- Next
- End Function
3) Task.WhenAny() - den schnellsten von mehreren Tasks auswerten
VB.NET-Quellcode
- Private Async Sub bt_Click(sender As Object, e As EventArgs) Handles btStart.Click
- txtResults.Clear()
- Dim client = New HttpClient()
- Dim sw = Stopwatch.StartNew
- Try
- Dim Tasks = Array.ConvertAll(_Urls, Function(url) ProcessURLAsync(url, client))
- Dim tskFirst = Await Task.WhenAny(Tasks)
- client.CancelPendingRequests()
- txtResults.AddLine("Length of the downloaded web site: ", Await tskFirst)
- txtResults.AddLine("Download complete.")
- Catch ex As OperationCanceledException
- txtResults.AddLine("Download canceled.")
- End Try
- txtResults.AddLine(sw.ElapsedMilliseconds, " Milliseconds")
- End Sub
- Async Function ProcessURLAsync(url As String, client As HttpClient) As Task(Of Integer)
- Console.WriteLine(url)
- Dim response As HttpResponseMessage = Await client.GetAsync(url)
- Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
- Return urlContents.Length
- End Function
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 mit
txtResults.AddLine("Length of the downloaded web site: ", tskFirst.Result)
. Weil ist besserer Stil, unnützes nicht zu benützen 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
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
- Async Function AccessTheWebAsync(ct As CancellationToken) As Task
- Dim client As HttpClient = New HttpClient()
- Dim Tasks = _Urls.Select(Function(url) ProcessURLAsync(url, client, _Cts.Token)).ToList
- While Tasks.Count > 0
- Dim tskFirst As Task(Of Integer) = Await Task.WhenAny(Tasks)
- Tasks.Remove(tskFirst)
- txtResults.AddLine("Length of download: ", tskFirst.Result)
- End While
- End Function
- Async Function ProcessURLAsync(url As String, client As HttpClient, ct As CancellationToken) As Task(Of Integer)
- Console.WriteLine(url)
- Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)
- Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()
- Return urlContents.Length
- End Function
HttpClient.CancelPendingRequests()
gecancelt wird, sondern zur Abwechslung mal wieder per CancellationToken
(vgl. auch Übung 1)Dieser Beitrag wurde bereits 12 mal editiert, zuletzt von „ErfinderDesRades“ ()