Warum ist Thread.Sleep so schädlich?

128

Ich sehe oft erwähnt, dass Thread.Sleep();es nicht verwendet werden sollte, aber ich kann nicht verstehen, warum dies so ist. Wenn Thread.Sleep();dies zu Problemen führen kann, gibt es alternative Lösungen mit demselben Ergebnis, die sicher wären?

z.B.

while(true)
{
    doSomework();
    i++;
    Thread.Sleep(5000);
}

ein anderer ist:

while (true)
{
    string[] images = Directory.GetFiles(@"C:\Dir", "*.png");

    foreach (string image in images)
    {
        this.Invoke(() => this.Enabled = true);
        pictureBox1.Image = new Bitmap(image);
        Thread.Sleep(1000);
    }
}
Burimi
quelle
3
Eine Zusammenfassung des Blogs könnte lauten: "Thread.sleep () nicht missbrauchen".
Martin James
5
Ich würde nicht sagen, dass es schädlich ist. Ich würde eher sagen, dass es so ist, als ob goto:es wahrscheinlich eine bessere Lösung für Ihre Probleme gibt als Sleep.
Standard
9
Es ist nicht genau das Gleiche wie goto, was eher einem Code-Geruch als einem Design-Geruch ähnelt. Es ist nichts Falsches daran, gotowenn ein Compiler s in Ihren Code einfügt : Der Computer wird nicht verwirrt. Ist Thread.Sleepaber nicht genau das gleiche; Compiler fügen diesen Aufruf nicht ein und es hat andere negative Konsequenzen. Aber ja, das allgemeine Gefühl, dass es falsch ist, weil es fast immer eine bessere Lösung gibt, ist sicherlich richtig.
Cody Gray
31
Jeder gibt Meinungen darüber ab, warum die obigen Beispiele schlecht sind, aber niemand hat eine umgeschriebene Version bereitgestellt, die Thread.Sleep () nicht verwendet und das Ziel der angegebenen Beispiele immer noch erreicht.
StingyJack
3
Jedes Mal, wenn Sie sleep()Code (oder Tests) eingeben, stirbt ein Welpe
Reza S

Antworten:

163

Die Probleme beim Anrufen Thread.Sleepwerden hier ganz kurz erklärt :

Thread.Sleephat seine Verwendung: Simulieren langwieriger Operationen beim Testen / Debuggen auf einem MTA-Thread. In .NET gibt es keinen anderen Grund, es zu verwenden.

Thread.Sleep(n)bedeutet, den aktuellen Thread für mindestens die Anzahl der Zeitscheiben (oder Thread-Quanten) zu blockieren, die innerhalb von n Millisekunden auftreten können . Die Länge einer Zeitscheibe ist bei verschiedenen Windows-Versionen / -Typen und verschiedenen Prozessoren unterschiedlich und liegt im Allgemeinen zwischen 15 und 30 Millisekunden. Dies bedeutet, dass der Thread fast garantiert länger als nMillisekunden blockiert . Die Wahrscheinlichkeit, dass Ihr Thread genau nach nMillisekunden wieder erwacht, ist so unmöglich wie unmöglich. Also, Thread.Sleepist sinnlos für das Timing .

Threads sind eine begrenzte Ressource. Die Erstellung dauert ungefähr 200.000 Zyklen und die Zerstörung ungefähr 100.000 Zyklen. Standardmäßig reservieren sie 1 Megabyte virtuellen Speicher für ihren Stapel und verwenden 2.000 bis 8.000 Zyklen für jeden Kontextwechsel. Dies macht jeden wartenden Thread zu einer riesigen Verschwendung.

Die bevorzugte Lösung: WaitHandles

Der am häufigsten gemachte Fehler ist die Verwendung Thread.Sleepeines while-Konstrukts ( Demo und Antwort , netter Blogeintrag ).

EDIT:
Ich möchte meine Antwort verbessern:

Wir haben 2 verschiedene Anwendungsfälle:

  1. Wir warten , weil wir eine bestimmte Zeitspanne, wenn wir sollten weiterhin (Verwendung Thread.Sleep, System.Threading.Timeroder Doppelgänger)

  2. Wir warten, weil sich einige Bedingungen irgendwann ändern ... Schlüsselwort (e) sind / sind einige Zeit ! Wenn sich die Bedingungsprüfung in unserer Code-Domäne befindet, sollten wir WaitHandles verwenden - andernfalls sollte die externe Komponente eine Art Hook bereitstellen ... wenn dies nicht der Fall ist, ist das Design schlecht!

Meine Antwort bezieht sich hauptsächlich auf Anwendungsfall 2

Andreas Niedermair
quelle
29
Ich würde 1 MB Speicher angesichts der heutigen Hardware nicht als große Verschwendung bezeichnen
Standard
14
@Default hey, diskutiere das mit dem Originalautor :) und es hängt immer von deinem Code ab - oder besser: der Faktor ... und das Hauptproblem ist "Threads sind eine begrenzte Ressource" - heutige Kinder wissen nicht viel über Effizienz und Kosten bestimmter Implementierungen, weil "Hardware ist billig" ... aber manchmal muss man sehr optd codieren
Andreas Niedermair
11
"Das macht jeden wartenden Thread zu einer riesigen Verschwendung", oder? Wenn eine Protokollspezifikation eine Pause von einer Sekunde erfordert, bevor Sie fortfahren, was wird dann 1 Sekunde warten? Irgendwo muss ein Thread warten! Der Aufwand für das Erstellen / Zerstören von Threads ist häufig irrelevant, da ein Thread aus anderen Gründen ohnehin ausgelöst werden muss und während der gesamten Lebensdauer des Prozesses ausgeführt wird. Es würde mich interessieren, wie Kontextwechsel vermieden werden können, wenn in einer Spezifikation steht: "Warten Sie nach dem Einschalten der Pumpe mindestens zehn Sekunden, bis sich der Druck stabilisiert hat, bevor Sie das Zufuhrventil öffnen."
Martin James
9
@CodyGray - Ich habe den Beitrag noch einmal gelesen. Ich sehe in meinen Kommentaren keine Fische irgendeiner Farbe. Andreas zog sich aus dem Web zurück: 'Thread.Sleep hat seine Verwendung: Simulieren langwieriger Operationen beim Testen / Debuggen eines MTA-Threads. In .NET gibt es keinen anderen Grund, es zu verwenden. Ich behaupte, dass es viele Apps gibt, in denen ein sleep () -Aufruf genau das ist, was erforderlich ist. Wenn Leigons von Entwicklern (denn es gibt viele) darauf bestehen, sleep () - Schleifen als Bedingungsmonitore zu verwenden, die durch Ereignisse / condvars / semas / was auch immer ersetzt werden sollten, ist dies keine Rechtfertigung für die Behauptung, dass es keinen anderen Grund gibt, sie zu verwenden '.
Martin James
8
In 30 Jahren MultiThreaded-App-Entwicklung (hauptsächlich C ++ / Delphi / Windows) habe ich in keinem der verfügbaren Codes einen Bedarf an Schlaf- (0) oder Schlaf- (1) Schleifen festgestellt. Gelegentlich habe ich solchen Code zu Debugging-Zwecken eingegeben, aber er hat nie den Weg zum Kunden gefunden. "Wenn Sie etwas schreiben, das nicht jeden Thread vollständig kontrolliert" - das Mikromanagement von Threads ist ein ebenso großer Fehler wie das Mikromanagement von Entwicklungsmitarbeitern. Für das Thread-Management ist das Betriebssystem vorgesehen - die bereitgestellten Tools sollten verwendet werden.
Martin James
34

SZENARIO 1 - Warten auf den Abschluss einer asynchronen Aufgabe: Ich bin damit einverstanden, dass WaitHandle / Auto | ManualResetEvent in einem Szenario verwendet wird, in dem ein Thread auf die Fertigstellung einer Aufgabe in einem anderen Thread wartet.

SZENARIO 2 - Timing während der Schleife: Als grober Timing-Mechanismus (while + Thread.Sleep) ist er jedoch für 99% der Anwendungen vollkommen in Ordnung, bei denen NICHT genau bekannt sein muss, wann der blockierte Thread "aufwachen" soll. Das Argument, das er benötigt 200k Zyklen zum Erstellen des Threads sind ebenfalls ungültig - der Timing-Loop-Thread muss trotzdem erstellt werden und 200k Zyklen sind nur eine weitere große Zahl (sagen Sie mir, wie viele Zyklen zum Öffnen einer Datei / eines Sockets / einer Datenbank aufgerufen werden sollen?).

Also, wenn + Thread.Sleep funktioniert, warum dann die Dinge komplizieren? Nur Syntaxanwälte wären praktisch !

Swab.Jat
quelle
Zum Glück haben wir jetzt TaskCompletionSource, um effizient auf ein Ergebnis zu warten.
Austin Salgat
13

Ich möchte diese Frage aus einer kodierungspolitischen Perspektive beantworten, die für niemanden hilfreich sein kann oder nicht. Aber besonders wenn Sie mit Tools arbeiten, die für 9-5 Unternehmensprogrammierer gedacht sind, verwenden Leute, die Dokumentation schreiben, Wörter wie "sollte nicht" und "nie", um zu bedeuten "tun Sie dies nicht, es sei denn, Sie wissen wirklich, was Sie tun tun und warum ".

Einige meiner anderen Favoriten in der C # -Welt sind, dass sie Ihnen sagen, dass Sie "niemals lock (this)" oder "niemals GC.Collect () aufrufen" sollen. Diese beiden werden in vielen Blogs und offiziellen Dokumentationen mit Nachdruck deklariert, und IMO sind vollständige Fehlinformationen. In gewisser Weise erfüllt diese Fehlinformation ihren Zweck, indem sie die Anfänger davon abhält, Dinge zu tun, die sie nicht verstehen, bevor sie die Alternativen vollständig erforscht haben. Gleichzeitig macht es jedoch schwierig, WIRKLICHE Informationen über Suchmaschinen zu finden, die alle scheinen auf Artikel zu verweisen, in denen Sie aufgefordert werden, nichts zu tun, ohne die Frage "Warum nicht?" zu beantworten.

Politisch läuft es darauf hinaus, was die Leute als "gutes Design" oder "schlechtes Design" betrachten. Die offizielle Dokumentation sollte nicht das Design meiner Anwendung bestimmen. Wenn es wirklich einen technischen Grund gibt, warum Sie sleep () nicht aufrufen sollten, sollte IMO in der Dokumentation angegeben werden, dass es völlig in Ordnung ist, es unter bestimmten Szenarien aufzurufen, aber möglicherweise einige alternative Lösungen anbieten, die szenariounabhängig oder für die anderen besser geeignet sind Szenarien.

Das eindeutige Aufrufen von "sleep ()" ist in vielen Situationen nützlich, in denen die Fristen in Echtzeit klar definiert sind. Es gibt jedoch ausgefeiltere Systeme zum Warten und Signalisieren von Threads, die berücksichtigt und verstanden werden sollten, bevor Sie mit dem Schlafen beginnen ( ) in Ihren Code, und unnötige sleep () - Anweisungen in Ihren Code zu werfen, wird im Allgemeinen als Anfängertaktik angesehen.

Jason Nelson
quelle
5

Es ist die 1) .spinning- und 2) .polling-Schleife Ihrer Beispiele, vor der die Leute warnen, nicht der Thread.Sleep () -Teil. Ich denke, Thread.Sleep () wird normalerweise hinzugefügt, um Code, der sich dreht oder sich in einer Abfrageschleife befindet, leicht zu verbessern, so dass es nur mit "schlechtem" Code verbunden ist.

Außerdem machen die Leute Sachen wie:

while(inWait)Thread.Sleep(5000); 

Dabei wird nicht threadsicher auf die Variable inWait zugegriffen, was ebenfalls Probleme verursacht.

Was Programmierer sehen möchten, sind die Threads, die von Events und Signaling and Locking-Konstrukten gesteuert werden. Wenn Sie dies tun, benötigen Sie Thread.Sleep () nicht, und die Bedenken hinsichtlich des thread-sicheren Variablenzugriffs werden ebenfalls beseitigt. Könnten Sie beispielsweise einen Ereignishandler erstellen, der der FileSystemWatcher-Klasse zugeordnet ist, und ein Ereignis verwenden, um Ihr zweites Beispiel auszulösen, anstatt eine Schleife durchzuführen?

Wie Andreas N. erwähnte, lesen Sie Threading in C # von Joe Albahari , es ist wirklich sehr, sehr gut.

Mike
quelle
Was ist also die Alternative zu dem, was in Ihrem Codebeispiel steht?
Shinzou
kuhaku - Ja, gute Frage. Offensichtlich ist Thread.Sleep () eine Verknüpfung und manchmal eine große Verknüpfung. Für das obige Beispiel ist der Rest des Codes in der Funktion unterhalb der Abfrageschleife "while (inWait) Thread.Sleep (5000);" eine neue Funktion. Diese neue Funktion ist ein Delegat (Rückruffunktion) und Sie übergeben sie an das, was das "inWait" -Flag setzt. Anstatt das "inWait" -Flag zu ändern, wird der Rückruf aufgerufen. Dies war das kürzeste Beispiel, das ich finden konnte: myelin.co.nz/notes/callbacks/cs-delegates.html
mike
Um sicherzugehen, dass ich es habe, wollen Sie Thread.Sleep()mit einer anderen Funktion umbrechen und diese in der while-Schleife aufrufen?
Shinzou
5

Sleep wird in Fällen verwendet, in denen unabhängige Programme, auf die Sie keinen Einfluss haben, manchmal eine häufig verwendete Ressource (z. B. eine Datei) verwenden, auf die Ihr Programm zugreifen muss, wenn es ausgeführt wird und wenn die Ressource von diesen verwendet wird andere Programme Ihr Programm ist daran gehindert, es zu verwenden. In diesem Fall, wenn Sie in Ihrem Code auf die Ressource zugreifen, setzen Sie Ihren Zugriff auf die Ressource in einen Try-Catch (um die Ausnahme abzufangen, wenn Sie nicht auf die Ressource zugreifen können) und setzen dies in eine while-Schleife. Wenn die Ressource frei ist, wird der Schlaf nie aufgerufen. Wenn die Ressource jedoch blockiert ist, schlafen Sie eine angemessene Zeit lang und versuchen erneut, auf die Ressource zuzugreifen (aus diesem Grund führen Sie eine Schleife durch). Beachten Sie jedoch, dass Sie eine Art Begrenzer auf die Schleife setzen müssen, damit es sich nicht um eine potenziell unendliche Schleife handelt.

Steve Greene
quelle
3

Ich habe einen Anwendungsfall, den ich hier nicht ganz sehe, und werde argumentieren, dass dies ein gültiger Grund für die Verwendung von Thread.Sleep () ist:

In einer Konsolenanwendung, in der Bereinigungsjobs ausgeführt werden, muss ich eine große Anzahl ziemlich teurer Datenbankaufrufe an eine Datenbank senden, die von Tausenden gleichzeitigen Benutzern gemeinsam genutzt wird. Um die DB nicht zu hämmern und andere stundenlang auszuschließen, benötige ich eine Pause zwischen den Aufrufen in der Größenordnung von 100 ms. Dies hängt nicht mit dem Timing zusammen, sondern nur damit, anderen Threads Zugriff auf die Datenbank zu gewähren.

Es ist harmlos, 2000-8000 Zyklen für die Kontextumschaltung zwischen Aufrufen zu verwenden, deren Ausführung 500 ms dauern kann, ebenso wie 1 MB Stapel für den Thread, der als einzelne Instanz auf einem Server ausgeführt wird.

Henrik R Clausen
quelle
Ja, die Verwendung Thread.Sleepin einer Konsolenanwendung mit einem Thread und einem einzigen Zweck wie der von Ihnen beschriebenen ist vollkommen in Ordnung.
Theodor Zoulias
-7

Ich stimme vielen hier zu, aber ich denke auch, dass es darauf ankommt.

Kürzlich habe ich diesen Code gemacht:

private void animate(FlowLayoutPanel element, int start, int end)
{
    bool asc = end > start;
    element.Show();
    while (start != end) {
        start += asc ? 1 : -1;
        element.Height = start;
        Thread.Sleep(1);
    }
    if (!asc)
    {
        element.Hide();
    }
    element.Focus();
}

Es war eine einfache Animationsfunktion, die ich verwendet Thread.Sleephabe.

Mein Fazit: Wenn es den Job macht, benutze es.


quelle
-8

Für diejenigen unter Ihnen, die kein gültiges Argument gegen die Verwendung von Thread.Sleep in SCENARIO 2 gesehen haben, gibt es wirklich eines: Der Anwendungs-Exit wird durch die while-Schleife aufgehalten (SCENARIO 1/3 ist einfach nur dumm, also nicht mehr wert Erwähnung)

Viele, die so tun, als wären sie auf dem Laufenden und schreien Thread.Sleep ist böse, haben keinen einzigen gültigen Grund für diejenigen von uns genannt, die einen praktischen Grund forderten, ihn nicht zu verwenden - aber hier ist es dank Pete - Thread.Sleep ist böse (kann leicht mit einem Timer / Handler vermieden werden)

    static void Main(string[] args)
    {
        Thread t = new Thread(new ThreadStart(ThreadFunc));
        t.Start();

        Console.WriteLine("Hit any key to exit.");
        Console.ReadLine();

        Console.WriteLine("App exiting");
        return;
    }

    static void ThreadFunc()
    {
        int i=0;
        try
        {
            while (true)
            {
                Console.WriteLine(Thread.CurrentThread.ThreadState.ToString() + " " + i);

                Thread.Sleep(1000 * 10);
                i++;
            }
        }
        finally
        {
            Console.WriteLine("Exiting while loop");
        }
        return;
    }
Swab.Jat
quelle
9
-, nein, Thread.Sleepist nicht der Grund dafür (der neue Thread und die kontinuierliche while-Schleife ist)! Sie können einfach die Thread.Sleep-line entfernen - et voila: das Programm wird nicht so gut beendet ...
Andreas Niedermair