TAP - Vermeidung von Async Sub [gelöst]

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

Es gibt 5 Antworten in diesem Thema. Der letzte Beitrag () ist von siycah.

    TAP - Vermeidung von Async Sub [gelöst]

    Hallo zusammen,

    @ISliceUrPanties meinte neulich

    ISliceUrPanties schrieb:

    Asynchrone Methoden geben immer Task zurück, auch wenn man keinen eigentlichen Rückgabewert hat. Nur für EventHandler sollte "async void" verwendet werden.
    und verwies auf das TAP. Das schwirrt mir seit einigen Tagen im Kopf rum und ich komm zu keinem sinnvollen Ergebnis.
    Folgende Programmsituation: Ein Programm weist eine Klasseninstanz zu Programmbeginn an, vorhandene PDF-Rechnungsdateien zu parsen und daraus Positionsinstanzklassen zu erstellen (Artikelnummer, Packungszahl, Preise). Dies ist ein langsamer Prozess, da mithilfe einer weiteren DLL, die mit einer DB kommuniziert, diese Daten harmonisiert werden (Artikelname, etc.). Auch kann es sein, dass zu mehreren Zeitpunken an Tag weitere Rechnungen hinzukommen. Also langwierig. Zusammenfassend:
    • Programm ruft InvoiceContainer auf
    • InvoiceContainer sammelt alle PDF-Dateien zusammen (schnell)
    • InvoiceContainer verwendet verschiedene Importer, um die Rechnungen verschiedener Lieferanten zu parsen (langsam, da PDF-Parsing)
    • InvoiceContainer sammelt mithilfe einer DLL und der dahinterliegenden DB weitere Daten der gelieferten Artikel zusammen und ergänzt die so die fehlenden Item-Instanzdaten (langsam)
    Nun die Frage, wie das laut TAP ablaufen soll.

    Denn ich habe bisher innerhalb von InvoiceContainer:

    VB.NET-Quellcode

    1. Private Async Sub _ExtractInvoiceItemsFrom(Invoice As Invoice)
    2. ReportStartOfInvoiceProcessing(Invoice.InvoiceID) 'Info ans GUI, dass gerade was importiert wird
    3. Await Task.Run(Sub() ExtractItemsOf(Invoice))
    4. ReportEndOfInvoiceProcessing() 'Info ans GUI, dass der Import beendet ist
    5. End Sub
    6. Private Sub ExtractItemsOf(Invoice As Invoice)
    7. Dim FileLines = GetFileLinesOf(Invoice.FileInfo.FullName) 'schnell
    8. Dim InvoiceItemFileLines = GetInvoiceItemFileLinesFrom(FileLines) 'langsam
    9. InvoiceImporter.ExtractInvoiceItemsFrom(InvoiceItemFileLines, Invoice) 'langsam
    10. End Sub


    Aber wenn ich @ISliceUrPanties richtig verstehe, soll man es nicht so machen. Wie dann?

    ##########

    Ja gut, die logische Schlussfolgerung: _ExtractInvoiceItemsFrom wird von einer Sub zu einer Function mit Task-Rückgabewert. Und der jeweilige EventHandler wird zur Async Sub gemacht, der auf den Task wartet. Denn die Aufgabe (wie so wahrscheinlich ziemlich alle Methoden) wird ja immer irgendwie ursprünglich durch einen EventHandler ausgelöst. Egal, ob es MainForm_Load ist, ein Timer, der tickt, ein Button, der geklickt wird oder ein FileSystemWatcher, der das Hinzukommen einer neuen PDF-File meldet.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

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

    Bei mir ist es derzeit:

    VB.NET-Quellcode

    1. Private Sub FrmInvoices_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    2. PrepareGui()
    3. End Sub
    4. Private Sub PrepareGui()
    5. '…
    6. ExtractInvoiceItemsFromAllInvoicesAndUpdateGui()
    7. End Sub
    8. Private Sub ExtractInvoiceItemsFromAllInvoicesAndUpdateGui()
    9. ExtractInvoiceItemsFromAllInvoices()
    10. UpdateInvoiceGuiParts()
    11. End Sub
    12. Private Sub ExtractInvoiceItemsFromAllInvoices()
    13. InvoiceContainer.ExtractInvoiceItemsFromAllInvoices()
    14. End Sub
    15. 'innerhalb der InvoiceContainer-Klasse
    16. Private Sub ExtractInvoiceItemsFromAllInvoices()
    17. Invoices.ForEach(Sub(x) ExtractInvoiceItemsFrom(x))
    18. End Sub
    19. Private Async Sub ExtractInvoiceItemsFrom(Invoice As Invoice)
    20. If Invoice.InvoiceItems.Any Then Return
    21. ReportStartOfInvoiceProcessing(Invoice.InvoiceID)
    22. Await Task.Run(Sub() ExtractItemsOf(Invoice))
    23. ReportEndOfInvoiceProcessing()
    24. End Sub


    Also: Async/Await ist derzeit noch tief im Code versteckt, und zwar in der InvoiceContainer-Klasse.
    Und ich muss jetzt wohl die meisten Subs zu Function … As Task machen und so das Async hochbubbeln, bis es bei FrmInvoices_Load angekommen ist.

    Interessant wird's später mit meinem AddHandlerCode, aber darum kümmere ich mich, wenn es soweit ist. Da weiß ich zwar, dass ich so arbeiten kann:

    VB.NET-Quellcode

    1. AddHandler BtnCollectAndShowAllInvoices.Click, Async Sub() Await Threading.Tasks.Task.Run(Sub() CollectAndShowAllInvoices())

    Aber als ästhetisch empfinde ich das nicht. Daher kommt wohl leider doch eher sowas am Ende raus:

    VB.NET-Quellcode

    1. AddHandler BtnCollectAndShowAllInvoices.Click, Sub() CollectAndShowAllInvoicesAsync()
    2. '…
    3. Private Async Sub CollectAndShowAllInvoicesAsync()
    4. Await …
    5. End Sub


    ##########

    Ah, hab doch nochmal einen Blick auf die TAP-Seite geworfen. Für mein Szenario ist wohl doch einfach nur Task.Run vorgesehen. Also kann ich quasi alles beim Alten lassen, und soll nur im EventHandler/der obersten Sub schreiben:

    VB.NET-Quellcode

    1. Private Async Sub FrmInvoices_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    2. Await Task.Run(Sub() PrepareGui())
    3. End Sub


    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „VaporiZed“ ()

    Mir fehlt hier eventuell ein Teil des Verständisses zu Async Await.

    VB.NET-Quellcode

    1. Private Sub Foo()
    2. Bar()
    3. End Sub
    4. Private Async Sub Bar()
    5. DoSome1()
    6. Await Task.Run(Sub() Baz)
    7. DoSome2()
    8. End
    Ich denke es ist so: DoSome1 läuft synchron. In Zeile 7 läuft Baz nebenläufig los, gleichzeitig returnt Bar, das heißt Foo müsste dann auch returnen.
    Irgentwann beendet Baz und dann wird DoSome2 anschließend nebenläufig ausgeführt.
    Während DoSome1, Baz, und DoSome2 alle relativ zueinander synchron laufen, läuft Baz und DoSome2 allgemein asynchron.

    In deinem Beispiel, läuft deswegen jede Invoice-ExtractItemsOf+ReportEnd parallel, abgesehen von der Verzögerung durch ReportStart.
    Nachdem also jeder ReportStart durch ist, müsste PrepareGui doch returnen. Warum muss das jetzt auch nochmal durch Await ausgelagert werden?
    Ist meine Grundannahme falsch? Ich sehe sonst den weiteren Vorteil nicht.

    Ich find das auch alles verwirrend, und ich habe auch in Erinnerung, dass ich mit so einer Hochbubbelei nicht zufrieden geworden bin, da muss es noch ein Verständnisproblem geben, dem ich nicht auf die Schliche komme.
    Nicht ganz. Deine Annahme für DoSome2 ist falsch.

    VB.NET-Quellcode

    1. Private Async Sub Bar()
    2. DoSome1() 'läuft im MainThread vor Start von Baz
    3. Await Task.Run(Sub() Baz)
    4. DoSome2() 'läuft im MainThread nach Ende von Baz
    5. End

    Daher braucht meine ReportEnd-Methode kein Invoke, um das GUI zu informieren, da es sich im Main-/GUI-Thread abspielt.

    Haudruferzappeltnoch schrieb:

    Nachdem also jeder ReportStart durch ist, müsste PrepareGui doch returnen.
    Ja, richtig.

    Das Hochbubbeln hat sich erledigt, siehe Ergänzung meines Vorposts.

    Bei der Sache geht es wohl darum, dass man Async Subs nur bei EventHandlern belässt, um sofort zu erkennen, ob da etwas nebenläufig passiert. Ich mach es nur etwas komplizierter, weil ich bei mir nur Einzeiler als Methodenaufruf bei den EventHandlern stehen haben will. Aber das ist mein Problem ^^
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    In Dotnet ist es so, dass Events nur dann funktionieren, wenn sie vom Typ void sind; in VB also eine sub.

    Dies ist aus Abwärtskompatibilitätsgründen so implementiert worden, da sonst das halbe Framework hätte umgeschrieben werden müssen, als Async/Await eingebaut wurde.

    Das liegt einfach an der Implementierung von Async/Await.
    Ganz einfach ausgedrückt, wenn du folgenden Code hast:

    C#-Quellcode

    1. public static Task Main() {
    2. // Do something
    3. await DoSomethingAsync();
    4. return 0;
    5. }
    6. private static Task DoSomethingAsync() {
    7. var result = await SomeLibrary.FunctionAsync();
    8. // Do something with result
    9. }


    dann generiert der Compiler für dich eine State-Machine.
    Async/Await ist nur syntactic sugar dafür.

    Microsoft hat vor ein paar Jahren einen schönen Blogeintrag geschrieben, was das ziemlich genau und einfach verständlich erklärt: devblogs.microsoft.com/dotnet/how-async-await-really-works/

    Im Übrigen dient der Task Rückgabewert genau dazu, die Statemachine auszuwerten :)
    Quellcode lizensiert unter CC by SA 2.0 (Creative Commons Share-Alike)

    Meine Firma: Procyon Systems

    Selbstständiger Softwareentwickler & IT-Techniker.