C# Programm anhalten, nicht beenden in WinForms?

2 Antworten

Vom Beitragsersteller als hilfreich ausgezeichnet

Dafür gibt's mathematische Formeln, such mal nach Stochastik :P

Aber nein, so eine Methode gibt's nicht, wird's auch nie geben. Das einzige was es gibt ist der Debugger, der kann die ganze Anwendung anhalten, dann funktioniert aber auch weder Ausgabe noch irgendein Button.

Du brauchst also einen Weg, wie Du die schleife pausieren kannst. Und damit das Programm danach nicht auch blockiert, muss die Schleife in einem eigenen Thread laufen.

Schau mal danach:

  • Task-based Asynchronous Pattern - das ist der aktuell beste Weg, mit Threads zu arbeiten
  • CancellationToken - Zum Abbrechen eines Tasks/Threads
  • ManualResetEvent - damit kannst Du aus mehreren Threads einen anderen Thread blockieren und wieder frei geben
  • lock - verhindert dass ein Stück Code von mehreren Threads gleichzeitig durchlaufen wird. Alternativ auch die Monitor-Klasse
  • Deadlock - der WorstCase-Fall eines Fehlers, der auftritt, wenn durch locks mehrere Thread endlos auf einander warten

Beachte aber auch, dass Zugriffe auf gemeinsam genutzte Daten aus mehreren Threads Probleme verursachen können. Die Daten-Quelle muss also multithreading-fähig sein oder Du verhinderst, dass der Zugriff auf die Quellen durch mehrere Threads geschehen kann.

Die oben genannten Klassen sind aber alle multithreading-fähig, immerhin sollen sie dafür sorgen, eigene Klassen dafür vorzubereiten.


Palladin007  15.12.2017, 12:48

Eine kleine Ergänzung noch:

Wenn deine Berechnung in einem eigenen Task läuft, musst Du aufpassen, dass die Control-Properties nicht geschrieben werden.

Ich würde - während die Berechnung läuft - gar keine Anzeige machen und stattdessen z.B. eine Loading-Animation anzeigen.

Erst wenn pausiert ist, zeigst Du an.

Auf diese Weise hast Du einen gewaltigen Vorteil:

Du brauchst dich nicht um die Zugriffe aus zwei Threads kümmern.

Läuft die Berechnung, schreibt sie fleißig in deine Variablen (z.B. numbers) und kein anderer Thread fast sie an. Beim pausieren, blockierst Du den Berechnungs-Task und liest danach (wenn der Thread pausiert ist) die Werte aus um sie anzuzeigen.

Wenn Du trotzdem live anzeigen willst, brauchst Du den Dispatcher von WinForms/WPF, der kann den übergebenen Delegaten im UI-Thread ausführen. Bei WPF ists glaube nur this.Dispatcher.Invoke()

Dann hast Du aber das Problem, dass das langsam ist. Du bremst nur durch das Anzeigen deine Berechnung stark aus.

Wenn Du einen Mix haben willst, kannst Du auch nach jedem 1000ten Durchlauf kurz anzeigen. Die nötige counter-Variable hast Du ja schon.

NathaLiiex3 
Beitragsersteller
 15.12.2017, 12:59
@Palladin007

oh vielen Dank für deine ausführliche Erklärung! :) Aber was ich dann immer noch nicht ganz verstehe, wenn ich jetzt also das Wait(), Set(); und Reset(); gemacht habe in der schleife, wie verbinde ich das ganze dann dahingehend das es mit dem Abbrechen Button "verbunden" ist.

Weil ich will ja da drauf klicken und dann soll er das machen.

Palladin007  15.12.2017, 13:15
@NathaLiiex3

Die Methoden Set() und Reset() liegen darin.

Beachte aber auch: Es kann sein, dass Du nach dem Set() noch nicht auf die Daten aus dem Thread zugreifen darfst, weil der noch nicht bei dem Wait() angekommen ist und wartet.

Das kannst Du lösen, wenn Du das Schreiben der Daten doch mit einem lock synchronisierst. Somit zwingst Du den UI-Thread dazu, auf den Berechnungs-Thread zu warten, wenn der noch nicht pausiert ist.

Das hab ich eben übersehen :/

Ich gebe zu, das ist nicht immer ganz einfach. Ich hab jetzt im Moment drei weitere Ideen, wie man das völlig anders lösen kann, alle mit ihren ganz eigenen Vor- und Nachteilen.

Da einen Überblick zu finden, dauert Tage, Wochen bis Monate. Besonders wenn man wie ich versucht, zu verstehen, wie die Tasks im Detail funktionieren.

Im Prinzip also:

Task StartCalculation()

{

return Task.Run(() =>

{

while (xyz)

{

_myResetEvent.Wait();

// Berechnung

lock (_mySyncRoot)

_ergebnis = abc;

}

});

}

void PauseOrResumeCalculation()

{

if (_isPaused)

{

_myResetEvent.Set();

_isPaused = false;

}

else

{

_myResetEvent.Reset();

_isPaused = true;

lock (_mySyncRoot)

textBox.Text = _ergebnis.ToString();

}

}

Der Tasks ist dafür da, dass die Berechnung async läuft.

Die erste Methode startet die Berechnung.

Die zweite Methode wird durch den Button aufgerufen und kann die Berechnung pausieren oder weiter laufen lassen.

Wenn ich nix übersehen habe, sollte das tun und threadsafe sein ...

PWolff  15.12.2017, 12:03

Ich würde erst mal einen BackgroundWorker probieren - der übernimmt die schwierigsten Dinge beim Taskwechsel schon mal

Palladin007  15.12.2017, 12:41
@PWolff

Der BackgroundWorker ist seit langem abgelöst. Stattdessen gibt's Tasks, die das ganze in sehr sehr sehr viel einfacher machen. Den gibt's nur noch, damit alte Projekte weiterhin bauen.

Die Tasks haben die gleichen Vorzüge, mit dem Unterschied, dass der geschriebene Code fast genau so aussieht, wie wenn man sich gar nicht um MultiThreading kümmert.

NathaLiiex3 
Beitragsersteller
 15.12.2017, 10:55

Cool, danke! Werd ich mir gleich mal alles anschauen.

Was ich noch des öfteren gelesen habe, war mit einer globalen bool Variable?

Palladin007  15.12.2017, 11:04
@NathaLiiex3

Globale Variablen gibt's in C# nicht und das ist auch gut so. Du kannst eine Variable in irgendeiner Klasse public static machen und dann von überall nutzen, aber damit baust Du dir nur Probleme.

Das WaitHandle (davon leitet ManualResetEvent ab) ist genau dafür da. Das Ding hat eine Wait-Methode, die so lange wartet, bis es wieder frei gegeben wird. Das ManualResetEvent ist eine Implementierung, die das interne Flag setzen und wieder frei geben kann. Damit kannst Du aus einem anderen Thread heraus steuern: "Warten!" oder "Lauf weiter"

NathaLiiex3 
Beitragsersteller
 15.12.2017, 11:18
@Palladin007

Ach so, okay, dachte nur weil viele Beispiele eben mit true und false gemacht wurden und dann bei dem Button beenden eben auf false gesetzt wird.

Danke dir! Ich probier es mal damit :) Kommt dass dann alles in meine while-Schleife mit rein?

Palladin007  15.12.2017, 12:37
@NathaLiiex3

In der While-Schleife kommt einfach ein .Wait() rein.

Da, wo blockiert werden soll, musst Du .Set() aufrufen. Wenn wieder frei gegeben werden soll muss da ein .Reset() stehen.

Aber wie gesagt: Die Berechnung muss asynchron laufen, sonst blockierst Du dir den GUI-Thread und hast danach keine Möglichkeit mehr, den wieder aufzuwecken.

Und dass viele einfach nur eine bool-Variable nutzen, hängt damit zusammen, dass viele keine Ahnung von den Möglichkeiten bei Multithreading haben :D Das ManualResetEvent ist ja nur ein Weg das zu managen, es gibt noch einige andere Möglichkeiten.

Viel anders macht's das ManualResetEvent aber auch nicht, mit dem Unterschied, dass Microsoft extremen Aufwand darauf geworfen hat, dass das über x-beliebige Threads hindurch sicher läuft.

Es geht sogar so weit, dass diese Klasse nicht in C# vor liegt, die ruft nur Methoden von der kernel32.dll auf, immerhin braucht Windows solche Funktionalität selber auch.

Das müsstest Du sonst selber bauen.

Du kannst dir ja mal da eine nicht-kernel32-Variante anschauen:

https://referencesource.microsoft.com/#q=ManualResetEventSlim

Das nutzt am Ende aber auch nur die Monitor-Klasse und die reicht die Aufrufe am Ende 1:1 an die .NET-Runtime weiter.

Und in der Runtime hat Microsoft wieder extreme Manpower darauf geworfen, dass jeder Aufruf atomar und sicher ist.

Wir reden hier schließlich von Klassen, die jedes einzelne Framework in x-facher Ausführung nutzt, sobald irgendwas mit Threads zu tun hat. Immerhin sind Fehler, die mit Multithreading zu tun haben, die ekligsten Fehler :D

Wenn Du es aber - zum Ausprobieren - doch selber machen willst, dann brauchst Du die Schlüsselwörter lock oder volatile.

Das lock zwingt die Threads dazu, zu warten, wenn ein Thread da gerade arbeitet. Das ist sozusagen das grundlegendste und einfachste Prinzip, die Zugriffe von mehreren Threads zu synchronisieren. Das lock wird vom Compiler in entsprechende Aufrufe gegen die Monitor-Klasse übersetzt.

Das volatile ist dagegen völlig anders. Das unterbindet die Compiler-Optimierung. Normal kann es sein, dass ein Thread die Änderung an einer Variable nicht sofort sehen kann - durch die besagte Optimierung. Wenn nun mehrere Threads fleißig herum schreiben, kann das zu Problemen führen.

Das volatile löst aber nicht das eigentliche Problem. Wenn Du z.B. prüfst ob Du einen Wert setzen darfst und ihn danach setzt, kann es sein, dass ein anderer Thread zwischen dem Prüfen und dem Setzen selber geschrieben hat. Das Ergebnis kann ein fehlerhafter Zustand sein.

Und ich kann dir aus eigener Erfahrung sagen: So einen Fehler zu finden ist die Hölle :D

regex9  16.12.2017, 16:10
@NathaLiiex3

Da ich das gerade lese: Natürlich gibt es in C# auch globale Variablen. Hierzu muss erklärt werden, dass die Begriffe global/lokal von einer Perspektive aus gemacht werden.

Bsp.:

public void doSomething() 
{
   int a = 1;

   {
      int b = a + 2;
   }
}

Die Variable a ist aus dem Kontext (Code-Block) heraus, in dem sich b befindet, eine globale Variable. Andersherum ist b nur lokal in ihrem Kontext bekannt (von außen betrachtet), aber generell global (in diesem betrachtet). Beide Variablen gelten innerhalb der Klasse als lokale Variablen, da sie nur in der Methode existieren.

Den größten Zugriffsbereich, den eine Variable auf sich bieten kann, ist ein statisch öffentliches Feld in einer sichtbaren Klasse (außerhalb von Klassenblöcken dürfen keine Variablen angelegt werden).

public class SomeClass
{
    public static int SomeField;
}

Solche Methoden sollten aber gemieden werden, in den meisten Fällen ist es bad practice.

Du rechnest nicht sondern probierst. Du hantierst hier mit nem PRNG statt nem CSPRNG. Du bekommst Zufallsdaten mit minderwertiger qualität.

Warum berechnest du nicht einfach die warscheinlichkeit?

Zum problem: mach dir doch nen bool den du auf knopfdruck verändern kannst und hänge die bedingung das der bool auf true steht in die while schleife. Selbe prinzip geht natürlich auch mit false, suchs dir raus.


Kieselsaeure  18.12.2017, 09:06

na dann :-) wenn du trotzdem noch an der variante die ich bevorzugt hätte interesse hast zu edukativen zwecken mache ich dir heute abend ein beispiel.

Kieselsaeure  18.12.2017, 22:55
@Kieselsaeure

ich glaube das wird doch eher gegen morgen. ich bin hundemüde von der arbeit. noch ne stunde arbeit und dann gehe ich erstmal schlafen. falls ich es vergesse erinner mich daran!

Kieselsaeure  15.12.2017, 13:55

ich hab auf ner vm windows installiert und visual studio code. wenn du möchtest kannst du mir ja mal das projekt schicken und ich versuche mal was zusammen zu basteln und sende es dir zurück

NathaLiiex3 
Beitragsersteller
 18.12.2017, 09:01
@Kieselsaeure

Hey, sorry musste am Freitag weg. Aber hab es jetzt geschafft anch langem probieren :D, mit einer bool Variable die ich vor der Schleife false; setze und in der Schleife mache ich dann ein break; und bei dem Abbrechen Button auf true; setzen.

Vielen Dank für deine Hilfe!

Kieselsaeure  15.12.2017, 13:48

ein break in der while schleife (falls du dich für die variante mit if entschieden hast) stoppt die while schleife. ich kann dir gerne ein beispiel schreiben zu deinem code. allerdings erst später. wäre nett wenn du sofern nichts dagegen spricht den code auf pastebin.org mit syntax higlighting für c# postest. das ist etwas angenehmer zu lesen und kopieren. und wie von @Palladin007 bereits erwähnt sollten die versuche in einem seperaten thread ausgeführt werden damit die main methode nicht blockiert und du dein stop event überhaupt setzen kannst (bool ändern). ich muss zugeben ich hab seit über 3 jahren nichtmehr mit c# gearbeitet aber ich denke das bekommen wir schon noch hin.

Kieselsaeure  15.12.2017, 12:40

kein problem. verzeihe mir falls ich manchmal unfreundlich wirke. das ist nicht böse gemeint :-). das mit dem abbrechen via button->bool verändern ist eine möglichkeit das problem zu lösen, ja. ist es für dich verständlich wie ich das meine?

NathaLiiex3 
Beitragsersteller
 15.12.2017, 12:56
@Kieselsaeure

Alles gut :-)

Ja, das hab ich verstanden, nur die Umsetzung hapert immer etwas :D Aber nicht dass du mich falsch verstehst, ich will des Programm ja anhalten und nicht komplett beenden. Weil mit einem break; würde er es mir ja beenden.

Und wenn ich dass dann in die while Schleife schreibe, daann macht er grad iwie gar nix bei mir :( :D

Kieselsaeure  15.12.2017, 11:53

warum denn eine if? du kannst es doch zu den bedingungen in der while zeile hinzufügen..? ansonsten ja. eine if mit entsprechendem break wäre auch möglich (aber unelegant).

NathaLiiex3 
Beitragsersteller
 15.12.2017, 12:36
@Kieselsaeure

Okay, ja sorry bin totaler Anfänger in C# noch und is bisschen verwirrend alles am Anfang :) Aber mit dem Abbrechen Button stimmt schon oder, dass da dann der bool auf false gemacht wird?

Palladin007  15.12.2017, 13:22
@NathaLiiex3

Wenn Du Anfänger bist, ist das vielleicht nicht das beste Projekt :D

Wenn es dir um's Beenden der Schleife und damit der Berechnung geht, dann ist das, was Leonard schreibt, das Mittel der Wahl.

Wenn Du die Berechnung pausieren willst, hast Du zwei Möglichkeiten:

  1. Du baust die Berechnung so um, dass Du zu einem beliebigen Zeitpunkt beenden kannst und ein erneutes Starten dort weiter arbeiten würde, wo es vorher aufgehört hat
  2. Du pausierst die Berechnung

In beiden Fällen muss die Berechnung aber in einem eigenen Thread (nichts anderes ermöglichen dir Tasks) liegen.

Der Grund ist, dass der GUI-Thread so lange blockiert ist, bis die Berechnung abgeschlossen ist. Das heißt, einen Button kannst Du nicht drücken.

Eine Möglichkeit, das zu "lösen" ist das Application.DoEvents()

Das ist allerdings sehr sehr unschön, weil Du damit unvorhergesehene Seiteneffekte verursachen kannst.

Richtig wäre, wenn Du das nicht brauchst, weil der GUI-Thread nie blockiert wird.

NathaLiiex3 
Beitragsersteller
 15.12.2017, 11:51

Hey,

also in die Schleife eine If mit true und dann bei dem Button den bool auf false?