Code Review zu Async

  • C#

Es gibt 14 Antworten in diesem Thema. Der letzte Beitrag () ist von VB.neter0101.

    Code Review zu Async

    Hallo,

    ich habe hier ein kurzes Minimalbeispiel zur Verwendung von Async/Await innerhalb einer GUI, siehe Code:

    C#-Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.ComponentModel;
    4. using System.Data;
    5. using System.Drawing;
    6. using System.IO;
    7. using System.Linq;
    8. using System.Text;
    9. using System.Threading.Tasks;
    10. using System.Windows.Forms;
    11. namespace LoadDataInGUI
    12. {
    13. public partial class Form1 : Form
    14. {
    15. public Form1()
    16. {
    17. InitializeComponent();
    18. }
    19. private async void Button1_ClickAsync(object sender, EventArgs e)
    20. {
    21. await Task.Run(() => PutData());
    22. }
    23. // Caller method to avoid async button
    24. private async void CallerMethod()
    25. {
    26. await Task.Run(() => PutData());
    27. }
    28. private void PutData()
    29. {
    30. // Load data from file
    31. string[] data = File.ReadAllLines(@"C:\...\4000Lines.txt");
    32. // Put data into GUI
    33. foreach (string line in data)
    34. {
    35. listBox1.Invoke(new Action(() => listBox1.Items.Add(line)));
    36. }
    37. }
    38. private void Button2_Click(object sender, EventArgs e)
    39. {
    40. // Load data from file
    41. string[] data = File.ReadAllLines(@"C:\...\4000Lines.txt");
    42. // Put data into GUI
    43. foreach (string line in data)
    44. {
    45. listBox1.Items.Add(line);
    46. }
    47. }
    48. private void Button3_Click_1(object sender, EventArgs e)
    49. {
    50. CallerMethod();
    51. }
    52. }
    53. }


    Dieser Implementierung sind vielleicht einige Ideen zu entnehmen. Ich habe zunächst einen Button2, mit dem ich den Code und den Zugriff auf das GUI Element (Listbox) synchron bewerkstellige. Soweit so gut, verursacht dies das bekannte "einfrieren" der Form. Zur Vermeidung des Einfrierens wurde der quasi gleiche Code in eine neue Methode PutData() ausgelagert, bei der Mittels Invoke auch von einem anderen Thread aus Zugriff auf das GUI Element erlaubt wird. Jetzt gibt es noch weitere Button Implementierungen. Zum einen den "asynchronen"-Button1, der die Methode PutData() aufruft, außerdem den Button3, der den asynchronen Code über eine CallerMethode aufruft, so erspare ich mir den "asynchronen" Button.

    Jetzt gibt es hier verschieden philosophische Ansätze, bsp. könnte man auch, wie der @ErfinderDesRades oft vorschlägt auch die "Dateneingabe" auslagern. Also erst Daten sammeln und dann zurück (z.B. synchron) in die GUI einspeisen. Meine Frage bzw. Fragen, richten sich nach der "perfekten" Implementierung und Verwendung von Async und Await. Wenn Ihr diese unterschiedlichen Ansätze seht, welcher davon wäre der idealste? Ist es überhaupt notwendig/sinnvoll einen Button nicht asynchron zu gestalten. Ich habe im Netzt verschiedene Implementierungen von Async/Await gesehen, häufig lassen sich diese jedoch auf zwei Formen reduzieren, entweder direkt den asynchronen Teil im Button (vergleiche hierzu die Implementierung in Button1) verwenden, oder es wird eine Hilfsmetode zu Rate gezogen, die dann den asynchronen Teil ausführt. Mich interessieren eure Meinungen.
    Hey,
    vielleicht keine Antwort auf deine Fragen speziell, aber finde deinen Part schon sehr nützlich, da ja oft die Frage aufkommt, warum sich das Programm bei größeren Aufgaben aufhängt bzw. einfriert.
    Habe es mal in VB.net übersetzen lassen und eine Funktion hinzugefügt, welche eine Textdatei mit 150.000 Zeilen generiert.
    Man kann hier sehr schön vergleichen wie es mit und ohne der Await-Methode läuft :)


    Für die, die es vielleicht interessiert:

    VB.NET-Quellcode

    1. Option Strict On
    2. Imports System.IO
    3. Public Class Form1
    4. Dim strFile As String = My.Computer.FileSystem.SpecialDirectories.Desktop & "\LargeFile.txt"
    5. Private Sub btnCreateTextFile_Click(sender As Object, e As EventArgs) Handles btnCreateTextFile.Click
    6. Dim fileExists As Boolean = File.Exists(strFile)
    7. For index As Integer = 1 To 150000
    8. File.AppendAllText(strFile, $"TEST {DateTime.Now}{Environment.NewLine}")
    9. Next
    10. End Sub
    11. Private Async Sub CallerMethod()
    12. Await Task.Run(Sub() PutData())
    13. End Sub
    14. Private Sub PutData()
    15. Dim data As String() = File.ReadAllLines(strFile)
    16. For Each line As String In data
    17. lbInhalt.Invoke(New Action(Sub() lbInhalt.Items.Add(line)))
    18. Next
    19. End Sub
    20. Private Sub btnOhneAwait_Click(sender As Object, e As EventArgs) Handles btnOhneAwait.Click
    21. Dim data As String() = File.ReadAllLines(strFile)
    22. For Each line As String In data
    23. lbInhalt.Items.Add(line)
    24. Next
    25. End Sub
    26. Private Sub btnMitAwait_Click(sender As Object, e As EventArgs) Handles btnMitAwait.Click
    27. CallerMethod()
    28. End Sub
    29. End Class



    Gruß,
    xored


    Meine Website:
    www.renebischof.de

    Meine erste App (Android):
    PartyPalooza
    Von der Struktur her ist Button1_Click leidlich richtig.
    Allerdings Konzeptions-Fehler, der sich bis in die Benamung fortpflanzt:
    Eine asynchrone PutData-Funktion kann es nicht geben, wenn mit "Put" das einspeisen in eine Listbox gemeint ist.
    Allenfalls das GetData kann asynchron erfolgen, das "Put" - also das Einspeisen in die Listbox - hat nach dem Await-Call zu erfolgen.

    Natürlich ists technisch möglich - wie gezeigt - jedes einzelne Item in einer Thread-übergreifenden Operation aus der Nebenläufigkeit hinüberzudelegieren.
    Das ist aber glaub sehr unwirtschaftlich, und dürfte den Gesamtvorgang erheblich verzögern (könnte man mal messen).

    Insgesamt ist das Beispiel nicht sehr ergiebig, weil 4000 Zeilen aus einer Datei einlesen geht ratzfatz - kein Anlass für Nebenläufigkeit.
    Und den anderen Teil - die 4000 Zeilen in die Listbox packen - das ist ja genau das, was nebenläufig garnet geht.



    In der CallerMethod sehe ich keinen Sinn. Was diese Methode enthält, kann ebensogut gleich im Button_Click passieren.
    Es ist zwar dran-kommentiert, der Sinn sei, async Button_Click zu vermeiden - aber warum das?
    async Button_Click an sich funzt wunderbar.

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

    @ErfinderDesRades Jou.
    @VB.neter0101 Dieses Beispiel würde ich nicht asynchron angehen.
    Was Du tun kannst:
    Unterdrücke das Updaten der ListBox während der Dauer ihrer Befüllung:
    docs.microsoft.com/de-de/dotne….beginupdate?view=net-5.0
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Danke für euer Feedback bzw. eure Meinungen!

    @ErfinderDesRades, ja die Benennung ist durchaus schlecht, das gebe ich zu, aber mir gehts vordergründig eher um die richtige Verwendung von Async und Await, dennoch ist der Einwand berechtigt. Das "Put" sollte eigentlich weniger fragwürdig sein, aber jetzt wo du es angesprochen hast, eine bessere Benennung wäre auch hier besser gewesen. Wenn du sagst, [...] "hat nach dem Await-Call zu erfolgen", wie ist das genau gemeint? Wenn ich deine Herangehensweise so richtig deute, bis du eher ein Freund davon, erst die Daten zu sammeln und danach einzuspeisen, richtig? Würdest du eher auf das Invoke in der For-Each verzichten und anstelle dieses erst die Daten in eine sagen wir mal Liste packen und daraus später die Listbox befüllen? Vllt. noch anbei, das mit der Liste und den 4000 Einträgen (veralteter Name, nachdem ich festgestellt hatte, dass 4000 Einträge recht schnell hinzugefügt werden können :)), dient dazu einen Prozess zu simulieren, der die GUI verwendet und gleichzeitig arbeitsintensiv ist, mir gehts im Detail darum, die Form noch Zugreifbar zu machen, bzw. die Formrelevanten Aktionen zugänglich zu lassen, die ein Nutzer ggf. noch verwenden müsste, auch wenn parallel ein arbeitsintensiver Vorgang ausgeführt wird.

    @RodFromGermany, das wusste ich noch nicht, dass es sowas auch gibt, guter Hinweis!

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „VB.neter0101“ ()

    Die ungünstige Benamung ist eine Folge des Konzept-Fehlers. Also letzterer ist das Problem - nämlich, dass du aus Nebenläufigkeit heraus beides machen willst - "Get" und "Put", nämlich Daten holen (lesen der Datei -> string[], "Get"), und Daten ins Gui tun (in die Listbox, "Put").
    Get und Put muss man aber trennen, nur Get kann nebenläufig erfolgen - Put nicht.
    Wäre dir das klar gewesen hättest du den Daten-Holer wohl auch entsprechend benamt GetData.

    VB.neter0101 schrieb:

    "Put hat nach dem Await-Call zu erfolgen", wie ist das genau gemeint?
    Du lieber Himmel - das ist so gemeint, wie's da steht:
    Put hat nach dem Await-Call zu erfolgen.
    Steht da doch!
    Anders kann ichs nicht sagen.
    Ich kanns höchstens noch vormachen:

    C#-Quellcode

    1. private async void Button1_Click(object sender, EventArgs e)
    2. {
    3. var lines = await Task.Run(() => GetData());
    4. PutData(lines);
    5. }
    6. private void PutData(string[] lines)
    7. {
    8. // Put lines into GUI
    9. foreach (string line in lines)
    10. {
    11. listBox1.Items.Add(line);
    12. }
    13. }

    VB.neter0101 schrieb:

    Wenn ich deine Herangehensweise so richtig deute, bis du eher ein Freund davon, erst die Daten zu sammeln und danach einzuspeisen, richtig? Würdest du eher auf das Invoke in der For-Each verzichten und anstelle dieses erst die Daten in eine sagen wir mal Liste packen und daraus später die Listbox befüllen?
    Ja, genau - wie du siehst. :thumbup:
    Hey @ErfinderDesRades, du musst ja nicht gleich ausrasten 8-) ... Ich frage ja nur nach (das was du geschrieben hast schwebte mir auch vor Augen), aber deine Ausführungen sind jetzt eindeutig und unmissverständlich.

    Wenn jedoch die GUI befüllt wird, dann müsste es deinem Beispiel zufolge jedoch zu dem bekannten "einfrieren" der Form kommen?!

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „VB.neter0101“ ()

    jedoch zu dem bekannten "einfrieren" der Form kommen?!
    Hallo VB.neter101 :) Das kommt auf die Menge der zu aktualisierenden Daten an. Der GUI-Thread ist der Hauptthread. Seine Aufgabe ist das Sich-bei-Windows-melden-dass-alles-gut-ist und die Interaktion mit dem User, daher auch das Aktualisieren der Form. Du kannst dem Hauptthread Aufgaben auf den Schreibtisch legen, und die erfüllt er auch, aber nach einer Minute, die sich nicht bei Windows gemeldet wurde, denkt Windows, dass das Programm nicht mehr reagiert (obwohl es rattert). Die Frage ist ist es in deinem aktuellen Programm wichtig, dass die GUI nicht für 5, 10, 20 Sekunden hängt? Ich meine damit: Der Nutzer soll wahrscheinlich eh nicht zwischendrin Knöpfe drücken? Muss ich gerade sagen; ich bin der, der für alles async nimmt :whistling: Mein Kantendetektionsprogramm läuft völlig asynchron, weil wegen Image processing, das dauert schonmal 30 Sekunden, bevor etwas fertig ist. – außer die Teile, die invoket werden müssen.
    Danke für deine Antwort. Ich meine einmal gelesen zu haben, dass die Forms Anwendung eine Single-Thread Anwendung ist, d.h. der Flaschenhals tritt dann unweigerlich irgendwann auf, egal ob ich jetzt die Daten vorher im Array habe oder aus der Datei lade, wobei letzteres als IO dann doch sehr viel langsamer wäre. Ich hab das gerade auch einmal implementiert und mit der Lösung von @RodFromGermany zusätzlich versehen, sodass die Listbox nicht ständig neu geupdated wird. Das Laden der Werte, sprich lesen aus der Datei mache ich asynchron, während dass in die GUI, vielmehr in die Listbox einfügen synchron geschieht. Tatsächlich merkt man hier bei, in meinem Fall 32000 Einträgen ein gewisses "Einfrieren", das hier aber recht kurzweilig ist, aber man merkt es dennoch! Die Frage ist, dann ob sich das "Einfrieren" hier überhaupt verhindern lässt?! Was mich etwas zweifeln lässt ist der Umstand, dass ich mir nicht vorstellen kann, dass es "normal" ist, dass die Form einfriert, wenn etwa 32K Einträge einer Listbox hinzugefügt werden.
    Das müssen die Anderen sagen, ob es „normal“ ist (ich würde ‚ja‛ sagen), oder ob es sich mittels bewährter Mittel verhindern lässt. Als ich mal ein Mikroprozessor-Projekt vor mir hatte (und das ist lange her, zu Fachhochschul-Zeiten), hat mir ein Bekannter gesagt, ich solle meine 17000 Messwerte gestückelt einlesen. Man nennt das Chunks. Also bissl einlesen, verarbeiten, nächsten Datensatz einlesen, verarbeiten, und so weiter. Nur dass es nicht um eine Form ging, sondern um zu wenig Speicher. Ich hatte nur 8 kB oder so.

    Quick&Dirty-Lösung:
    Application.DoEvents() Das wird allerdings nicht gern gesehen. Man sagt: „Musst du es zu oft nutzen, dann biste irgendwo falsch abgebogen“.

    Soweit ich weiss hat zumindest die Listview einen virtual mode. Dort werden nicht alle items der Listview hinzugefügt sondern es gibt events für die anzuzeigenden Einträge. D.h. du kannst die Daten einmal einlesen in ein Array/List und dann im Listview Event einfach nur die entsprechenden Items mit dem gegebenen Index anzeigen.
    Die List benötigt lediglich die Anzahl der gesamten Items.

    docs.microsoft.com/en-us/dotne….virtualmode?view=net-5.0
    Das ist meine Signatur und sie wird wunderbar sein!

    VB.neter0101 schrieb:

    Hey @ErfinderDesRades, du musst ja nicht gleich ausrasten 8-)
    Ja - haste recht - hätte ich garnet müssen, sorry.

    VB.neter0101 schrieb:

    Wenn jedoch die GUI befüllt wird, dann müsste es deinem Beispiel zufolge jedoch zu dem bekannten "einfrieren" der Form kommen?!
    Ja.
    Wenn das Befüllen des GUIs lange dauert muss es einfrieren.
    Zum Glück sind Controls ziemlich gut Geschwindigkeits-Optimiert - diese Geschichte mit .BeginUpdate/.EndUpdate findet sich in vielen, wenn nicht sogar allen Listen-Controls wieder (und auch im Treeview).
    DGV kennt darüberhinaus (oder stattdessen) den VirtualMode, wo es nur so tut, als habe es 10000 Datensätze geladen, und in Wirklichkeit lädts die iwie sukzessive nach.
    Wenn sich "Befüllen des GUIs" auch wirklich aufs reine Daten-Reintun beschränkt, und nicht noch iwas umständliches gerechnet wird (das soll ja nebenläufig passiert sein), ja dann müssen schon ziemlich viele Daten kommen, bevor man ein Gui zum merklichen Einfrieren bringt.

    Aber es bleibt dabei: Wenn Gui-Befüllen lange dauert, solange frierts.

    Und ja, da wird man sich behelfen - zB Stichwort "Chunks" ist auch ein Ansatz - dass man nicht alles auf einmal einfüllt, sondern in Paketen. Dann frierts halt mehrmals, aber jeweils entsprechend kürzer.
    Vielleicht kriegt man ja auch hin, erstmal 500 Datensätze anzuzeigen, und während der User guckt kann man heimlich noch paar Chunks nachladen - da merkt er vlt. garnet sofort, dasses gefroren ist.

    Oder Paging: Auch eine Form des Chunkens, aber dem User offensichtlich: Ich kriege hier "nur" die ersten 1000 Datensätze, und die nächsten 1000 muss ich eben nochmal anfordern.

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

    Danke für eure Antworten @Bartosz, @Mono, @ErfinderDesRades!

    Ich halte das auch für ein angemessenes Verfahren die Daten in Pakete aufzuteilen. Die Frage war theoretisch motiviert, ich halte es eher für schlechtes Design so viele Werte in eine GUI zu laden. Dennoch ist interessant, wie man mit derlei Fällen umgeht. Das die GUI einfriert, gilt dies nur für Forms-Anwendungen, oder ist das z.B. auch bei WPF der Fall? Ich habe die Hoffnung, dass eine neuere Technologie vllt. bessere/effizientere Ansätze aufweist, als die komplette Form einfrieren zu lassen :)
    afaik kann Gui nicht nebenläufig befüllt werden.
    GUI, "Grafic User Interface" - das bedeutet ja: dem User wird was angezeigt.
    Bei Nebenläufigkeit arbeitet der Thread auf einer Arbeitskopie der Daten. Bedeutet: dieselben Daten haben in verschiedenen Threads verschiedene Werte.
    Wie soll eine GUI das anzeigen?
    @ErfinderDesRades, das würde ich so als Antwort stehen lassen. Alternativen zur Performance Verbesserung hatten wir ja oben bereits angesprochen. Ich habe mich auch mal an verschiedene Implementierungen gewagt und musste feststellen, dass das Pausieren der Aktualisierung der Listbox, wie @RodFromGermany vorgeschlagen hat resümierend betrachtet, mit am meisten geholfen hat, außerdem besticht das auch durch seine "Schlichtheit". Danke, dass Ihr euch hier auf diese eher theoretisch motivierte Fragestellung so gut eingelassen habt :)