Windows Forms Applikation reagiert kurzzeitig nicht

  • C#

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von EaranMaleasi.

    Windows Forms Applikation reagiert kurzzeitig nicht

    Hallo,

    ich habe mal wieder eine kleine Windows Forms Anwendung für mich geschrieben. Das Programm als solches Funktioniert auch, nur mich treibt die Frage umher, wie man verhindern kann das Windows Forms Anwendungen einfrieren.
    Ich will das einmal näher ausführen. Angenommen ich habe eine aufwendige rekursive Funktion, die aufgerufen wird sobald ein Button gedrückt wurde. Dann kommt es häufig vor, dass das Programm einfriert. Je nachdem wie aufwendig der Funktionsaufruf ist, kann dies auch mal etwas dauern bis zu einer Minute, in der das Fenster nicht ansprechbar ist! Im Task Manager erscheint dann auch "Keine Rückmeldung". Das Programm selbst fängt sich aber und nach etwa einer Minute habe ich wieder Kontrolle über das Programm.

    Das sind beispielsweise Dinge, die passieren, wenn man eine sehr große Textdatei nach einem speziellen Suchstring durchsucht oder bei Verzeichnisstrukturen.

    Meine Frage ist nun, wie kann man den generell verhindern, dass Windows Forms Applikationen einfrieren? ISt Multithreading da eine Möglichkeit? Fernen würde ich gerne diesen Prozess auch mit einer Progressbar visualisieren wollen. Das dürfte ja vermutlich bekannt sein, dass es Suchprogramme gibt, die dann eben nicht einfrieren, sondern dem Nutzer beispielsweise über eine Progressbar informieren, dass derzeit noch gesucht wird, das würde ich gerne koppeln wollen. Meine Frage ist daher, wie gestaltet man sowas?
    Da ist Multithreading denke ich mal bereits das korrekte Stichwort, um aber aus dem Thread/Task heraus auch informationen rauslesen zu können, wirst du auch einen DeleGateSub benötigen.
    Im folgenden Beispiel habe ich auf der Form1, eine Progressbar mit .max 10000, .min 0 und einen Button zum starten.

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Dim Upd As New Update_UI(AddressOf Update_UISub)
    2. Delegate Sub Update_UI()
    3. Public Sub Update_UISub()
    4. ProgressBar1.PerformStep()
    5. End Sub
    6. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    7. StartTask()
    8. End Sub
    9. Sub StartTask()
    10. Dim IntensiveTaskThread As Thread = New Thread(Sub() IntensiveTask())
    11. IntensiveTaskThread.SetApartmentState(ApartmentState.STA)
    12. IntensiveTaskThread.Start()
    13. End Sub
    14. Sub IntensiveTask()
    15. For i As Integer = 0 To 9999
    16. Thread.Sleep(500)
    17. Me.Invoke(Upd)
    18. Next
    19. End Sub
    If Energy = Low Then
    Drink(aHugeCoffee)
    Else
    Drink(aHugeCoffeeToo)
    End If
    Das geht kürzer und moderner mit Async/Await:

    VB.NET-Quellcode

    1. Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    2. Await Threading.Tasks.Task.Run(Sub()
    3. For i As Integer = 0 To 9999
    4. Threading.Thread.Sleep(500)
    5. Me.Invoke(Sub() ProgressBar1.PerformStep())
    6. Next
    7. End Sub)
    8. End Sub
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.
    @VaporiZed
    Das geht kürzer und moderner mit Async/Await:


    Woooow ! das ist ja mal genial, mehr als deutlich weniger zu tippen, Danke dir !!!
    Aber eine Frage noch, mit Async/Await, wird dabei der Haupthread, so nicht etwas mehr belastet als mit einem extra thread ? oder hab ich da nur etwas falsch im Kopf (nur relevant wenn man mal eine sehr rechenintensive methode hat)
    If Energy = Low Then
    Drink(aHugeCoffee)
    Else
    Drink(aHugeCoffeeToo)
    End If
    Mit Task.Run() (neuer, abgespeckte Version von Task.Factory.StartNew()), bzw. Task.Factory.StartNew() (etwas älter) erreicht man keine wirkliche Nebenläufigkeit. Die Codeausführung stoppt beim Erreichen des Await-Schlüsselwortes und wartet asynchron auf die Beendigung dieser Methode. Solltest Du in die Versuchung kommen, hier sehr zeitintensive Berechnungen durchzuführen, dann wirst Du zwar merken, dass die Form nicht einfriert, aber bis zur Beendigung auch nicht wirklich etwas passiert, was nach dem Aufruf kommt. Hier wird ein Thread aus dem (auf dem Betriebssystem) bereits vorhandenen Threadpool genutzt.

    Async/Await würde vornehmlich für I/O-Aufgaben (Lesen von Dateien, Netzwerkkommunikation usw.) gemacht. Nicht unbedingt für rechenintensive Aufgaben. Behalte das im Hinterkopf. Für intensive Berechnungen würde ich weiter auf die altbekannten Threads setzen, wenn sich der Aufwand für das Erstellen eines solchen rentiert. Threads erzeugen kostet Zeit, Threads aus dem Pool sind bereits da.

    Hier ein paar Beispiele... (C#)

    C#-Quellcode

    1. private async void button1_Click(object sender, EventArgs e)
    2. {
    3. await Task.Run(() =>
    4. {
    5. double result = 0;
    6. for (int i = 0; i < 60000000; i++)
    7. {
    8. result += Math.Pow(120, i);
    9. }
    10. MessageBox.Show(result.ToString());
    11. });
    12. MessageBox.Show("hello");
    13. }


    Zuerst kommt die MessageBox mit dem Ergebnis aus dem Task, dann die Messagebox hello. Die Codeausführung wartet solange bis Await fertig ist.

    C#-Quellcode

    1. private async void button1_Click(object sender, EventArgs e)
    2. {
    3. new Thread(() =>
    4. {
    5. double result = 0;
    6. for (int i = 0; i < 60000000; i++)
    7. {
    8. result += Math.Pow(120, i);
    9. }
    10. this.textBox1.Invoke(new Action(() =>
    11. {
    12. MessageBox.Show(result.ToString());
    13. }));
    14. }).Start();
    15. MessageBox.Show("hello");
    16. }


    Hier kommt sofort die MessageBox hello und die Ausgabe des Ergebnisses später. Wirkliche Nebenläufigkeit.

    C#-Quellcode

    1. private async void button1_Click(object sender, EventArgs e)
    2. {
    3. new Action(() =>
    4. {
    5. double result = 0;
    6. for (int i = 0; i < 60000000; i++)
    7. {
    8. result += Math.Pow(120, i);
    9. }
    10. MessageBox.Show(result.ToString());
    11. }).BeginInvoke(null, null);
    12. MessageBox.Show("hello");
    13. }


    Selbiges gilt für den Aufruf von BeginInvoke... Soweit ich weiß, wird hier auch ein PoolThread verwendet, nagel mich aber darauf nicht fest. Auch hier wird Nebenläufigkeit erreicht. Die MessageBox hello kommt sofort, das Ergebnis später.

    Außerdem solltest Du darauf verzichten, innerhalb von Threads oder Tasks ständig auf Controls zuzugreifen. Mache das nur ab und an, weil auch dies kostet enorm viel Zeit und kann die Anwendung zum stocken bringen. Ein Update einer ProgressBar muss nicht bei jedem Schleifendurchgang erfolgen.

    Async/Await ist ein wunderbares Werkzeug, wenn man es einzusetzen weiß. Wie gesagt vornehmlich wurde es für I/O-Aufgaben geschaffen.
    Die Unendlichkeit ist weit. Vor allem gegen Ende. ?(
    Manche Menschen sind gar nicht dumm. Sie haben nur Pech beim Denken. 8o

    How to turn OPTION STRICT ON
    Why OPTION STRICT ON
    Ich fasse gedanklich Async-Await-Prozeduren für mich so zusammen:
    • arbeite Teil 1 ab (bei Post#3: zwischen Zeile#1 und #2 - im dortigen Code also nix)
    • erwarte (await) das Ende von folgender Aufgabe (Z#2 bis #7)
    • mach danach weiter mit Teil 2 (zwischen Z#7 und #8 - auch nicht vorhanden)
    der Arbeitsablauf »echter Nebenläufigkeit«, so wie Du es nennst, kann man aber auch erreichen:

    VB.NET-Quellcode

    1. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    2. DoTheFirstPart()
    3. DoTheHeavyJob()
    4. DoTheSecondPart()
    5. End Sub
    6. Private Sub DoTheFirstPart
    7. MessageBox.Show("Part 1")
    8. End Sub
    9. Private Async Sub DoTheHeavyJob()
    10. Await Threading.Tasks.Task.Run(Sub()
    11. For i As Integer = 0 To 9999
    12. Threading.Thread.Sleep(500)
    13. Me.Invoke(Sub() ProgressBar1.PerformStep())
    14. Next
    15. End Sub)
    16. MessageBox.Show("endlich fertig")
    17. End Sub
    18. Private Sub DoTheSecondPart
    19. MessageBox.Show("Part 2")
    20. End Sub

    Nun taucht nach der 1. MessageBox sofort die 2. auf - obwohl die nebenläufige Aufgabe noch am werkeln ist.

    (Dies ist keine Gegendarstellung zu Deinem Post, sondern eine Ergänzung)
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.
    @VaporiZed hey!!!

    Du hast natürlich recht mit Deinem Beispiel. Nur leider läuft man bei diesem Muster schnell in die Gefahr, den UI-Thread ins Deadlock zu schicken. Die passiert, wenn man beispielsweise in einem Task eine Berechnung ausführt und auf das Ergebnis wartet.

    VB.NET-Quellcode

    1. Imports System.Threading
    2. Public Class Form1
    3. Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    4. MessageBox.Show(Pow(10, 10).ToString())
    5. MessageBox.Show(PowTaskAsync(10, 10).Result.ToString())
    6. End Sub
    7. Private Function Pow(a As Integer, b As Integer) As Double
    8. Return Math.Pow(a, b)
    9. End Function
    10. Private Async Function PowTaskAsync(a As Integer, b As Integer) As Task(Of Double)
    11. Return Await Task.Run(Function()
    12. Return Math.Pow(a, b)
    13. End Function)
    14. End Function
    15. End Class


    Dem Task-Object wird ein "Continuation"-Context mitgegeben, was in diesem Fall der UI-Thread ist, denke ich. Der UI-Thread wartet nun auf das Ergebnis, dies wird aber nicht eintreffen können, da der Task ebenfalls auf den UI-Thread wartet, bis dieser frei ist -> Deadlock.

    Die Lösung hierfür ist folgende...

    VB.NET-Quellcode

    1. IImports System.Threading
    2. Public Class Form1
    3. Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    4. MessageBox.Show(Pow(10, 10).ToString())
    5. MessageBox.Show((Await PowTaskAsync(10, 10)).ToString())
    6. MessageBox.Show(Pow(10, 20).ToString())
    7. End Sub
    8. Private Function Pow(a As Integer, b As Integer) As Double
    9. Return Math.Pow(a, b)
    10. End Function
    11. Private Async Function PowTaskAsync(a As Integer, b As Integer) As Task(Of Double)
    12. Return Await Task.Run(Function()
    13. Return Math.Pow(a, b)
    14. End Function)
    15. End Function
    16. End Class


    Es muss mit Await "gewartet" werden, um das Deadlock zu vermeiden. Somit ist man wieder beim erwähnten Prozess, dass hier keine echte Nebenläufigkeit stattfindet. Das nur am Rande. Du hast natürlich recht und ich hätte mich etwas genauer ausdrücken sollen. Hier warten einige Fallstricke, die man beachten muss.
    Die Unendlichkeit ist weit. Vor allem gegen Ende. ?(
    Manche Menschen sind gar nicht dumm. Sie haben nur Pech beim Denken. 8o

    How to turn OPTION STRICT ON
    Why OPTION STRICT ON
    Ich habe nun selber einmal versucht mein Programm zu parallelisieren. Ich verwende eine Methode, die mir Verzeichnisse anzeigen kann über einen rekursiven Aufruf. Das ist recht anstrengend für das Programm, wodurch sich die Forms Anwendung zunächst nicht berühren lässt, das Fenster friert schlicht ein, bis der Aufruf abgearbeitet wurde.

    Ich habe nun folgendes versucht:

    C#-Quellcode

    1. Parallel.Invoke(() =>
    2. {
    3. superHeavyFunction();
    4. });

    Diesen Code rufe ich innerhalb eines Buzton Klick Events auf:

    C#-Quellcode

    1. private void button1_Click(object sender, EventArgs e)
    2. {
    3. Parallel.Invoke(() =>
    4. {
    5. superHeavyFunction();
    6. });
    7. }


    Mein Problem ist an dieser Stelle, dass das Programm zwar macht was es soll, nur eben nicht parallel. Ich hatte gehofft hiermit das "einfrieren" des Fensters zu verhindern.

    Alternativ habe ich es über das hier versucht, was mir eine Exception "Ungültiger threadübergreifender Vorgang" eingebrockt hat:


    C#-Quellcode

    1. private async void button1_Click(object sender, EventArgs e)
    2. {
    3. await System.Threading.Tasks.Task.Run(() =>
    4. {
    5. superHeavyFunction();
    6. });
    7. }


    Wie sehe denn in meinem Fall eine richtige Parallelisierung aus, sodass die GUI nicht einfriert?
    Parallelisierung, und Multithreading sind, wenn auch eng verwandt, zwei paar Stiefel.

    Parallelisierung bedeutet, eine Aufgabe Parallel auszuführen. Beispiel hierfür wäre das parallele Durchsuchen einer großen Liste nach irgendwelchen Daten. Man teilt diese Liste in x kleinere Einheiten und Sucht dann Parallel nach dem gewünschten Objekt. Dadurch wird Suchzeit ungefähr um den Faktor X verringert, vorausgesetzt, du besitzt einen Prozessor, der X Threads gleichzeitig ausführen kann.

    Multithreading bedeuted lediglich, dass man Aufgaben auf einen anderen Thread auslagert, um entweder höhere Performance zu erreichen, oder um andere Dinge nicht zu blockieren. Man sieht hierbei, das Parallelisierung dem ersten Aufgabengebiet entspringt, wir jedoch zweites erreichen möchten.

    Async/Await sind in der Hinsicht jedoch noch ein Stückchen anders. Anstatt die Aufgabe direkt in einen neuen Thread auszulagern, was z.B. durch die Erstellung eines Thread-Objektes, oder die direkte Nutzung des ThreadPools geschieht (der Thread wurde zuvor vom OS erstellt), wird hierbei ein Task erstellt, der Konkurrent zur "Hauptaufgabe" (die GUI) ausgeführt wird. Dies bedeutet, dass innerhalb deselben Threads die CPU-Zeit zwischen deinem Task und der GUI aufgeteilt werden. Dadurch wird die GUI nicht blockiert, da sie von der CPU Zeit des Threads genügend abbekommt um nicht einzufrieren, ebenso wird aber auch deine Aufgabe gleichzeitig bearbeitet. Inwiefern nun die Ausführung deiner Aufgabe dadurch gebremst wird, kann dir keiner genau sagen, da der TaskScheduler bzw. das Betriebsystem, wenn es bemerkt, dass dein Task ordentlich Rechenleistung braucht oder sehr lange läuft, diesen in einen neuen Thread auslagert. Auch kann man, wenn man Anstatt Task.Run() einen Task mit bestimmten Überladungen des Konstruktors erstellt dem TaskScheduler bereits mitteilen, dass dieser Task besser in einen Thread ausgelagert werden sollte. Ob und wann der Scheduler darauf reagiert, ist jedoch seine Sache.

    Um nun auf dein Konkretes Problem zurück zu kommen... Was genau macht denn deine superHeavyFunction()?
    SIMDoku (Simple Dokumentenverwaltung)
    Mein Lernprojekt um die verschiedensten Facetten der .NET Entwicklung zu erkunden.
    GitHub

    VB Paradise Dark Theme
    Inoffizieller VB-Paradise Discord.