c# warten aber nicht GUI stoppen?

3 Antworten

sobald ich Thread.Sleep(2000); eingeben wird automatische das komplette programm gewartet .

Weil es im UI-Thread läuft und wenn Du mit "Thread.Sleep" den UI-Thread pausierst, pausiert auch die UI.

Also entweder Du lebst damit oder nutzt "await Task.Delay".
Aber mach es bloß nicht so, wie MrAmazing2 schreibt!!! Da kannst Du dir auch gleich eine Zeitbombe ans Bein binden...

Wenn Du schon mit async/await arbeitest, dann musst Du richtig asynchron arbeiten, das heißt, Du musst wirklich begriffen haben, was das bedeutet und was Du dabei beachten musst und Du musst es von Anfang bis Ende durchziehen! Und kannst Du nicht einfach mittendrin aufhören, mit async/await zu arbeiten, entweder ganz oder gar nicht.

Und nein, "Task.Delay(2000).Wait()" solltest Du auch nicht nutzen, im UI-Thread (und manchen anderen auch) hättest Du damit einen Deadlock.

Viel besser wäre, Du beseitigst den Grund, warum Du 2 Sekunden warten willst, das ist nämlich fast immer unnötig oder eine Folge von anderen Fehlern. Übrigens ist eine sehr häufige Ursache, warum man scheinbar grundlos warten muss, falsch eingesetztes async/await, wie es MrAmazing2 zeigt.
Für alle realen Anwendungsfälle gibt's verschiedene andere Ansätze, wie blockierende Methoden, asynchrone Methoden, Events, Callbacks und noch einiges mehr.

Woher ich das weiß:Berufserfahrung – C#.NET Senior Softwareentwickler

MrAmazing2  08.04.2022, 18:58

Du hast jetzt nur genannt, warum das keine gute Idee ist. Aber was schlägst du denn als Alternative vor?

Sagen wir, im GUI sollte auf Knopfdruck ein Countdown angezeigt werden (3..2..1..) (zählt im Sekundentagt nach unten). Und nach 3 Sekunden soll do_something() ausgeführt werden.

Wie würdest du es machen? Womit würdest du das verzögern?

0
Palladin007  08.04.2022, 19:00
@MrAmazing2

Ich habe Alternativen genannt, aber ein einfaches Warten ist - ohne vollständig ausgearbeitetes async/await - eben nie einfach.

Und deine Aufgabe: Mit einem Timer.

2

Ich weiß nicht was Du im Endeffekt vorhast? ...aber ... Irgendwie bist Du konzeptionell total auf dem Holzweg.

Lass Deine produktiven Aufgaben in einem Backgroundworker laufen und teile der GUI über Ereignistrigger lediglich mit, das es neue Ergebnisse gibt und liefere den Status als Parameter mit .

Um den Rest kümmert sich die GUI (Formsobjekt) ganz von selbst.

Du brauchst dich nicht darum zu kümmern ob und was warten soll. Das ist Aufgabe des Taskmanagements von Windows!

(die "sleeps" im Backgroundworker sind nur dazu Da um diesen künstlich zu verlangsamen, also "richtig viel Arbeit zu simuleren".)

using System;
using System.Windows.Forms;
using System.ComponentModel;

class FibonacciNumber : Form
{
  //[STAThread]
  static void Main()
  {
    Application.EnableVisualStyles();
    Application.Run(new FibonacciNumber());
  }

  private StatusStrip progressStatusStrip;
  private ToolStripProgressBar toolStripProgressBar;
  private NumericUpDown requestedCountControl;
  private Button goButton;
  private TextBox outputTextBox;
  private BackgroundWorker backgroundWorker;
  private ToolStripStatusLabel toolStripStatusLabel;
  private int requestedCount;

  public FibonacciNumber()
  {
    Text = "Fibonacci";
     
    // Prepare the StatusStrip.
    progressStatusStrip = new StatusStrip();
    toolStripProgressBar = new ToolStripProgressBar();
    toolStripProgressBar.Enabled = false;
    toolStripStatusLabel = new ToolStripStatusLabel();
    progressStatusStrip.Items.Add(toolStripProgressBar);
    progressStatusStrip.Items.Add(toolStripStatusLabel);

    FlowLayoutPanel flp = new FlowLayoutPanel();
    flp.Dock = DockStyle.Top;

    Label beforeLabel = new Label();
    beforeLabel.Text = "Calculate the first ";
    beforeLabel.AutoSize = true;
    flp.Controls.Add(beforeLabel);
    requestedCountControl = new NumericUpDown();
    requestedCountControl.Maximum = 1000;
    requestedCountControl.Minimum = 1;
    requestedCountControl.Value = 100;
    flp.Controls.Add(requestedCountControl);
    Label afterLabel = new Label();
    afterLabel.Text = "Numbers in the Fibonacci sequence.";
    afterLabel.AutoSize = true;
    flp.Controls.Add(afterLabel);
     
    goButton = new Button();
    goButton.Text = "&Go";
    goButton.Click += new System.EventHandler(button1_Click);
    flp.Controls.Add(goButton);

    outputTextBox = new TextBox();
    outputTextBox.Multiline = true;
    outputTextBox.ReadOnly = true;
    outputTextBox.ScrollBars = ScrollBars.Vertical;
    outputTextBox.Dock = DockStyle.Fill;

    Controls.Add(outputTextBox);
    Controls.Add(progressStatusStrip);
    Controls.Add(flp);

    backgroundWorker = new BackgroundWorker();
    backgroundWorker.WorkerReportsProgress = true;
    backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
    backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
    backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
  }

  private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
  {
    // This method will run on a thread other than the UI thread.
    // Be sure not to manipulate any Windows Forms controls created
    // on the UI thread from this method.
    backgroundWorker.ReportProgress(0, "Working...");
    Decimal lastlast = 0;
    Decimal last = 1;
    Decimal current;
    if (requestedCount >= 1)
    { AppendNumber(0); }
    if (requestedCount >= 2)
    { AppendNumber(1); }
    for (int i = 2; i < requestedCount; ++i)
    {
      // Calculate the number.
      checked { current = lastlast + last; }
      // Introduce some delay to simulate a more complicated calculation.
      System.Threading.Thread.Sleep(500);
      AppendNumber(current);
      backgroundWorker.ReportProgress((100 * i) / requestedCount, "Working...");
      // Get ready for the next iteration.
      lastlast = last;
      last = current;
    }

    backgroundWorker.ReportProgress(100, "Complete!");
  }

  private delegate void AppendNumberDelegate(Decimal number);
  private void AppendNumber(Decimal number)
  {
    if (outputTextBox.InvokeRequired)
    { outputTextBox.Invoke(new AppendNumberDelegate(AppendNumber), number); }
    else
    { outputTextBox.AppendText(number.ToString("N0") + Environment.NewLine); }
  }
  private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
  {
    toolStripProgressBar.Value = e.ProgressPercentage;
    toolStripStatusLabel.Text = e.UserState as String;
  }

  private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  {
    if (e.Error is OverflowException)
    { outputTextBox.AppendText(Environment.NewLine + "**OVERFLOW ERROR, number is too large to be represented by the decimal data type**"); }
    toolStripProgressBar.Enabled = false;
    requestedCountControl.Enabled = true;
    goButton.Enabled = true;
  }

  private void button1_Click(object sender, EventArgs e)
  {
    goButton.Enabled = false;
    toolStripProgressBar.Enabled = true;
    requestedCount = (int)requestedCountControl.Value;
    requestedCountControl.Enabled = false;
    outputTextBox.Clear();
    backgroundWorker.RunWorkerAsync();
  }
}

...das sieht auf den Ersten blick fürchterlich kompliziert aus. Im Endeffekt muss man sich jedoch nur darum Kümmern was produktiv zu erledigen ist und was man den Auszulösenden Events mit auf den Weg gibt. Ein Backgroundworker macht einfach sein Ding und kümmert sich nicht um die GUI und die GUI nicht um den Backroundworker.

Die GUI reagiert lediglich wenn sie ein Ereignis gesendet bekommt welches für sie interessant ist


Palladin007  08.04.2022, 21:43
Lass Deine produktiven Aufgaben in einem Backgroundworker laufen und teile der GUI über Ereignistrigger lediglich mit

Seufz :)
Es ist nicht falsch, aber ...

BackgroundWorker sind Mottenkiste, sie waren der Vorgänger der Tasks.
Klar, man kann sie nutzen, aber sie liefern im Grunde das gleiche wie Tasks, nur deutlich komplizierter. Im kleinen Rahmen "mal eben so" mag das funktionieren, aber sobald mehrere solcher Aufgaben aufeinander folgen sollen, werden die BackgroundWorker zur Ausgeburt der Hölle - ich durfte mal so einen Start-Prozess anpassen, der aus fünf aufeinander folgenden BackgroundWorkern bestand ^^

Übrigens hat der BackgroundWorker auch ein technischen Nachteil: Er arbeitet mit nur einem Thread. Wenn der Thread vom ThreadPool kommt (kann man sicher einstellen), dann ist der Thread solange blockiert, bei größeren Anwendungen kann das zum Problem werden. Tasks verwenden - wenn man sie richtig nutzt - immer einen anderen Thread vom Pool, sodass kein Thread lange blockiert ist. Und für die UI wird immer der UI-Thread verwendet - das ist die "Magie", die sie uns versprechen.

Ein Backgroundworker macht einfach sein Ding und kümmert sich nicht um die GUI und die GUI nicht um den Backroundworker.

Das stimmt so nicht ganz.

Soweit ich mich erinnere (ist lange her) arbeitet auch der BackgroundWorker mit dem SynchronizationContext und die UI bietet eine Ableitung davon an. Die Tasks arbeiten auch damit, um Aufgaben auf den UI-Thread zurück zu synchronisieren.

Das ist auch ein Detail, was man sowohl beim BackgroundWorker als auch den Tasks immer im Hinterkopf behalten sollte, denn so nützlich das auch sein mag, dass es auch nach hinten los gehen, wenn man nicht beachtet, wo man nun welchen SynchronizationContext hat.

Die GUI reagiert lediglich wenn sie ein Ereignis gesendet bekommt welches für sie interessant ist

Das funktioniert aber auch nur beim BackgroundWorker. Der sorgt (soweit ich mich erinnere) dafür, dass für die UI relevante Events (also z.B. Progress) auch auf den UI-Thread ausgeführt werden. Für andere Events von anderen Klassen gilt das natürlich nicht, wenn so ein Event aus einem anderen Thread aus aufgerufen wird, muss man selber synchronisieren oder man verwendet Tools (z.B. Tasks ;) ), die das übernehmen.

0
Erzesel  08.04.2022, 22:20
@Palladin007

Ich hatte lediglich eine halbwegs passable Lösung im Hinterkopf, um jemanden ,der schon daran scheitert die GUI nicht einschlafen zu lassen einen relativ simplen Weg anzubieten. (man muss sich immer vor Augen halten, das ein Anfänger auf diesem Gebiet erstmal von überhaupt davon wegkommt das alles brav nacheinander verarbeitet...)

Ich denke nicht, das es der richtige Weg ist, einem Anfänger beizubringen das man an einer seriellen Befehlsfolge mal schnell etwas assyc "vorbeischiebt".

Naja und so schlimm "abgegriffen" sind die Worker ja auch nicht.

0
Palladin007  08.04.2022, 22:46
@Erzesel
Ich denke nicht, das es der richtige Weg ist, einem Anfänger beizubringen das man an einer seriellen Befehlsfolge mal schnell etwas assyc "vorbeischiebt".

Punkt für dich :D

Meine Empfehlung wäre aber sowieso eine ganz andere: Nicht warten.
Ich denke wie Du, dass das das eigentliche Problem im Konzept liegt, das hier indirekt dazu zwingt, diese zwei Sekunden zu warten. Besser wäre es, wenn dieser Grund beseitigt wird, dass gar nicht gewartet werden muss. Und für die meisten anderen Fällen, tun's auch Timer.

Oder anders gesagt: Die beste Komplexität ist gar keine Komplexität :)

Naja und so schlimm "abgegriffen" sind die Worker ja auch nicht.

Nicht abgegriffen, aber er bietet keine nennenswerten Vorteile und hat sogar einige (zugegeben kleine) Nachteile - neben der schnell unübersichtlich werdenden Event getriebenen Arbeitsweise.

Klar, einfache Aufgaben kann man problemlos mit einem BackgroundWorker umsetzen und da spricht auch nichts gegen, aber dabei bleibt es eben nur sehr selten. Außerdem sind Tasks bei einfachen Aufgaben genauso einfach zu benutzen, wie BackgroundWorker, wenn nicht sogar einfacher.

Wenn es komplexer wird, dann zeigen sich plötzlich die Nachteile beider Arbeitsweisen und mMn. gewinnen die Tasks, da der Codefluss im Wesentlichen gleich bleibt. Bei den BackgroundWorkern muss man dagegen mit massig Events hantieren.

1

Thread.Sleep() blockiert den kompletten Thread - In dem natürlich auch das GUI läuft.

Benutze stattdessen eine async-Funktion mit await Task.Delay(...).

Also

private async void do_something()
{
    doOneThing();
    await Task.Delay(2000);
    doAnotherThingAfter2Seconds();
}

Und in deinem Haupt-Code dann einfach die Funktion aufrufen:

do_something();
Woher ich das weiß:Hobby – Programmieren ist mein Hobby & Beruf

Palladin007  08.04.2022, 18:34
Thread.Sleep() blockiert den kompletten Thread - In dem natürlich auch das GUI läuft.

Korrekt.

Benutze stattdessen eine async-Funktion mit  await Task.Delay(...).

Wärst Du jetzt in Reichweite, würde ich dich schlagen - mit einer Keule ...

Ich durfte schon Projekte überarbeiten, wo so gearbeitet hast. Du würdest das nicht empfehlen, wenn Du wüsstest, was für eine abartige scheiß Arbeit das ist, den Mist wieder raus zu kriegen.

Also für die ganz dummen:

=== KEIN ASYNC VOID !!!!!! ===

Naja, es gibt Ausnahmen, aber das sind eben Ausnahmen und sollten gut überlegt sein...

0
MrAmazing2  08.04.2022, 19:00
@Palladin007
Du würdest das nicht empfehlen, wenn Du wüsstest, was für eine abartige scheiß Arbeit das ist, den Mist wieder raus zu kriegen.

Das weiß ich ganz gut. Zwar von JavaScript, aber ist hier genauso. Das async frisst sich in den Code. Deshalb die ganz einfache Lösung: Den Mist nicht wieder entfernen, sondern für immer mit async arbeiten.

0
Palladin007  08.04.2022, 19:02
@MrAmazing2
Das weiß ich ganz gut. Zwar von JavaScript, aber ist hier genauso. Deshalb die ganz einfache Lösung: Den Mist nicht wieder entfernen, sondern für immer mit async arbeiten.

Ich musste nicht das async entfernen, sondern es richtig machen.

Heißt: Überall einen Task/ValueTask zurück geben.

Und im Idealfall noch überall einen CancellationToken durch reichen, notwendig ist das aber nicht.

0
MrAmazing2  08.04.2022, 19:05
@Palladin007
Heißt: Überall einen Task/ValueTask zurück geben.

Ah, damit man es aus einem synchronen Kontext mit Task.Run aufrufen kann? Oder zu welchem Zweck?

0
Palladin007  08.04.2022, 19:15
@MrAmazing2

Damit andere Methoden darauf warten können, bis die Methode zuende ist.

Mit deinem Code wäre die Methode zuende, sobald sie beim Task.Delay ankommt - kann lustig werden, wenn die doAnotherThingAfter2Seconds-Methode zwei Sekunden später aufgerufen wird und sich dann mit irgendwas Anderem beißt.

1
MrAmazing2  08.04.2022, 19:18
@Palladin007
Damit andere Methoden darauf warten können, bis die Methode zuende ist.

Aber das würde doch den Thread der wartenden Methode, und somit das GUI, für 2 Sekunden blockieren?

kann lustig werden, wenn die doAnotherThingAfter2Seconds-Methode zwei Sekunden später aufgerufen wird und sich dann mit irgendwas Anderem beißt.

Ich denke darauf achtet man schon, dass sich da nichts beist ... Also ich zumindest
Der Fragesteller vlt. nicht, wenn er nichtmal weiß, was async/await ist, das könnte sein...

0
Palladin007  08.04.2022, 19:28
@MrAmazing2
Aber das würde doch den Thread der wartenden Methode, und somit das GUI, für 2 Sekunden blockieren?

Würde es nicht - gerade das ist doch das Versprechen von async/await.
Das setzt natürlich voraus, dass die nächste aufrufende Methode ebenfalls korrekt asynchron arbeitet und die nächste auch und die nächste auch ...

Also entweder ganz asynchron, oder gar nicht, mittendrin aufhören funktioniert nicht - oder nur mit sehr problematischen Hacks.

Ich denke darauf achtet man schon, dass sich da nichts beist ... Also ich zumindest

Ich hab sowas schon in ein paar produktiven Projekten gesehen.

Sobald es größer wird, als eine Hand voll Klassen, kann man nicht mehr darauf achten, oder man weiß es nicht und versucht das Problem dann zu umgehen, indem 2,5 Sekunden gewartet wird oder baut anderen Murks, der das ganze immer schwieriger zu warten macht.

Viel cooler wäre es dagegen doch, wenn man auf sowas gar nicht erst achten muss? Man muss es nur richtig machen ;)

1
Erzesel  08.04.2022, 21:18
@Palladin007

...eigendlich alles nur "Rettunsanker"...

....wenn man schon in der GUI produktive Aufgaben ausführen will, sollte man diese komplett in eigene Treads "auslagern" und es den Threads überlassen der Gui mitzuteilen , wofür diese sich interessieren könnte. Die GUI läuft doch ohnehin in einer Endlosschleife , welche nichts weiter tut als evtl. eingetragene Events abzuklappern...

1
Palladin007  08.04.2022, 21:33
@Erzesel

Ich korrigiere dich Mal:

"wenn man in der GUI produktive Aufgaben ausführen will, sollte das ganz schnell bleiben lassen".

So gefällt es mir besser :D

Ganz unterschreiben kann ich das mit den Threads aber nicht, zumindest nicht bei C#. Threads sind im Handling einfach so viel komplexer und man muss an so vielen Stellen mehr beachten, dass es sich mMn. nicht lohnt.

Deshalb gibt's ja die Tasks, die kapseln das alles und bieten ein Konstrukt, das für GUIs eigentlich ideal ist - war ja ursprünglich auch genau dafür gedacht.
Und klar, Tasks arbeiten auch mit Threads, aber da kommt noch deutlich mehr dazu (z.B. kann ein Task in seiner Lebenszeit mit vielen verschiedenen Threads arbeiten, oder nie den aktuellen Thread verlassen, oder mehrere gleichzeitig), weshalb man beides möglichst unterscheiden sollte.

Aber mit Beidem kann man nicht vernünftig arbeiten, wenn man nicht weiß, was da für Probleme dran hängen können.

3