Wie man richtig und falsch schlafen kann

    Vor nicht allzu langer Zeit durchlief uns ein guter Artikel über den schrecklichen Leistungsstand moderner Software ( Original in Englisch , Habré-Übersetzung ). Dieser Artikel erinnerte mich an ein Anti-Muster von Code, das häufig vorkommt und im Allgemeinen irgendwie funktioniert, aber hier und da zu einem leichten Leistungsverlust führt. Nun, wissen Sie, eine Kleinigkeit, mit der die Hände fixiert werden, wird nicht erreicht. Das einzige Problem ist, dass ein Dutzend solcher "Kleinigkeiten", die an verschiedenen Stellen des Codes verstreut sind, zu Problemen führt, wie "Ich habe den letzten Intel Core i7 und das Scrollen ruckelt".

    Ich spreche vom Missbrauch der Sleep- Funktion.(Fall kann je nach Programmiersprache und Plattform variieren). Was ist Schlaf? Die Dokumentation beantwortet diese Frage sehr einfach: Es ist eine Pause bei der Ausführung des aktuellen Threads für eine bestimmte Anzahl von Millisekunden. Es ist die ästhetische Schönheit des Prototyps dieser Funktion zu beachten:

    voidSleep(DWORD dwMilliseconds);

    Nur ein Parameter (extrem klar), keine Fehlercodes oder Ausnahmen - funktioniert immer. Es gibt nur wenige derart schöne und klare Funktionen!

    Mehr Respekt für dieses Feature, wenn Sie lesen, wie es funktioniert
    Die Funktion geht an den OS-Thread-Scheduler und teilt mit: „Wir möchten mit meinem Thread die uns zugewiesene CPU-Zeit jetzt und jetzt für viele Millisekunden in der Zukunft aufgeben. Gib den Armen! Etwas überrascht von dieser Großzügigkeit, gibt der Scheduler im Namen des Prozessors Dankbarkeitsfunktionen aus, gibt die verbleibende Zeit an den nächsten Willen (und es gibt immer solche) und enthält nicht den Stream, der den Sleep veranlasst hat, für die angegebene Anzahl von Millisekunden für den Ausführungskontext zu bieten. Schönheit

    Was könnte schief gehen? Dass Programmierer diese wunderbare Funktion verwenden, ist nicht das, was sie beabsichtigt.

    Und es ist für die Softwaresimulation eines externen Pausenprozesses bestimmt, der durch einen realen Pausenprozess definiert wird.

    Korrekte Beispielnummer 1


    Wir schreiben die Anwendung "Uhr", in der Sie einmal pro Sekunde die Nummer auf dem Bildschirm (oder die Position des Pfeils) ändern müssen. Die Sleep-Funktion passt hier perfekt: Wir haben wirklich für einen genau definierten Zeitraum (genau eine Sekunde) nichts zu tun. Warum nicht schlafen?

    Die richtige Beispielnummer 2


    Wir schreiben den Controller des Mondscheinapparates der Brotmaschine . Der Betriebsalgorithmus wird von einem der Programme definiert und sieht folgendermaßen aus:

    1. Gehe zu Modus 1.
    2. 20 Minuten darin arbeiten
    3. Wechseln Sie in den Modus 2.
    4. 10 Minuten darin arbeiten
    5. Herunterfahren

    Auch hier ist alles klar: Wir arbeiten mit der Zeit, sie wird vom technologischen Prozess bestimmt. Der Schlafmodus ist akzeptabel.

    Nun wollen wir uns Beispiele für einen falschen Gebrauch von Schlaf anschauen.

    Wenn ich ein Beispiel für falschen C ++ - Code benötige, gehe ich zum Code- Repository des Texteditors Notepad ++. Sein Code ist so schrecklich, dass jedes Anti-Pattern definitiv da sein wird, ich habe sogar einen Artikel darüber geschrieben . Lass mich nopadpadik ++ diesmal nicht im Stich! Mal sehen, wie der Schlaf darin verwendet wird.

    Schlechtes Beispiel Nummer 1


    Beim Start prüft Notepad ++, ob bereits eine andere Instanz des Prozesses ausgeführt wird. Wenn ja, sucht es nach dem Fenster, sendet eine Nachricht und schließt sich. Um seinen anderen Prozess zu erkennen, wird eine Standardmethode verwendet - der globale Name mutex. Der folgende Code wurde geschrieben, um nach Fenstern zu suchen:

    if ((!isMultiInst) && (!TheFirstOne))
    {
        HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL);
        for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i)
        {
            Sleep(100);
            hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL);
        }
        if (hNotepad_plus)
        {
        ...
        }
        ...
    }


    Der Programmierer, der diesen Code geschrieben hat, hat versucht, das Fenster des bereits laufenden Notepad ++ zu finden, und stellte sich sogar eine Situation vor, als zwei Prozesse gleichzeitig buchstäblich gestartet wurden. Daher hat der erste Prozess bereits einen globalen Mutex erstellt, jedoch noch kein Editorfenster. In diesem Fall wartet der zweite Prozess auf die Erstellung des Fensters "5 mal 100 ms". Infolgedessen warten wir entweder gar nicht oder verlieren bis zu 100 ms zwischen dem Zeitpunkt der eigentlichen Erstellung des Fensters und dem Verlassen des Schlafmodus.

    Dies ist das erste (und eines der wichtigsten) Muster, das den Schlafmodus verwendet. Wir warten nicht auf den Beginn des Events, sondern "für einige Millisekunden haben Sie plötzlich Glück". Wir warten so sehr, dass es einerseits den Benutzer nicht sehr stört, andererseits haben wir die Chance, auf das Ereignis zu warten, das wir brauchen. Ja, der Benutzer bemerkt beim Start der Anwendung möglicherweise keine Pause von 100 ms. Wenn jedoch eine solche Praxis "von der Glatze ein wenig warten" akzeptiert und im Projekt zulässig ist, kann dies dazu führen, dass wir bei jedem Schritt auf die kleinsten Gründe warten. Hier sind es 100 ms, es sind weitere 50 ms und hier sind es 200 ms - und jetzt verlangsamt sich unser Programm bereits "aus irgendeinem Grund ein paar Sekunden".

    Außerdem ist es nur ästhetisch unangenehm, Code zu sehen, der lange Zeit funktioniert, da er schnell arbeiten könnte. In diesem Fall können Sie die Funktion verwendenSetWindowsHookEx , abonniert das HSHELL_WINDOWCREATED -Ereignis - und Sie erhalten sofort eine Benachrichtigung über das Erstellen eines Fensters. Ja, der Code wird etwas komplizierter, aber buchstäblich 3-4 Zeilen. Und wir gewinnen bis zu 100 ms! Und am wichtigsten ist, dass wir die Funktionen des bedingungslosen Wartens nicht mehr verwenden, wenn das Warten nicht bedingungslos ist.

    Schlechtes Beispiel Nummer 2


    HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL);
    int sleepTime = 1000 / x * y;
    ::Sleep(sleepTime);

    Ich habe nicht wirklich verstanden, was genau und wie lange auf diesen Code in Notepad ++ gewartet wurde, aber ich sah oft die übliche Gegenmethode "Einen Stream starten und warten". Die Menschen warten auf verschiedene Dinge: den Beginn der Arbeit eines anderen Streams, den Erhalt einiger Daten daraus, das Ende seiner Arbeit. Zwei Dinge sind hier auf einmal schlecht:

    1. Multithreading-Programmierung ist notwendig, um Multithreading-Aktionen durchzuführen. Ie Beim Start des zweiten Threads wird davon ausgegangen, dass wir im ersten Thread noch etwas tun werden. Zu diesem Zeitpunkt erledigt der zweite Thread andere Arbeit, und der erste Thread, der seine Arbeit beendet hat (und vielleicht etwas länger gewartet hat), erhält sein Ergebnis und verwendet es irgendwie. Wenn wir gleich nach dem Start des zweiten Streams anfangen zu "schlafen" - warum ist das überhaupt nötig?
    2. Sie müssen richtig warten. Für das ordnungsgemäße Warten gibt es bewährte Verfahren: die Verwendung von Ereignissen, Wartefunktionen und Rückrufen. Wenn wir darauf warten, dass der Code im zweiten Thread arbeitet, starten Sie ein Ereignis und signalisieren Sie es im zweiten Thread. Wenn wir auf die Fertigstellung des zweiten Threads warten, verfügt C ++ über eine wunderbare Thread-Klasse und deren Join-Methode (oder wiederum plattformspezifische Methoden wie WaitForSingleObject und HANDLE in Windows). Es ist einfach dumm zu warten, bis die Arbeit in einem anderen Thread „einige Millisekunden“ erledigt ist, denn wenn wir kein Echtzeitbetriebssystem haben, kann Ihnen niemand garantieren, wie lange dieser zweite Thread beginnt oder bis zu einem bestimmten Zeitpunkt seiner Arbeit geht.

    Schlechtes Beispiel Nummer 3


    Hier sehen wir einen Hintergrund-Thread, der auf einige Ereignisse wartet.

    classCReadChangesServer
    {
    ...
    voidRun(){
            while (m_nOutstandingRequests || !m_bTerminate)
            {
                ::SleepEx(INFINITE, true);
            }
        }
        ...
    voidRequestTermination(){
            m_bTerminate = true;
        ...
        }
        ...
        bool m_bTerminate;
    };
    

    Man muss zugeben, dass hier nicht Sleep verwendet wird, sondern SleepEx , was intelligenter ist und das Warten auf bestimmte Ereignisse (z. B. den Abschluss asynchroner Vorgänge) unterbrechen kann. Aber es hilft überhaupt nicht! Tatsache ist, dass die while-Schleife (! M_bTerminate) jedes Recht hat, auf unbestimmte Zeit zu arbeiten, wobei die von einem anderen Thread aufgerufene RequestTermination () - Methode ignoriert wird und die Variable m_bTerminate auf true zurückgesetzt wird. Über die Ursachen und Folgen davon habe ich in einem früheren Artikel geschrieben . Um dies zu vermeiden, sollten Sie etwas verwenden, das garantiert korrekt zwischen Threads funktioniert: ein Atom, ein Ereignis oder ähnliches.

    Ja, formal ist SleepEx nicht für das Problem der Verwendung einer regulären booleschen Variablen zum Synchronisieren von Threads verantwortlich, da dies ein separater Fehler einer anderen Klasse ist. Warum wurde es in diesem Code möglich? Weil der Programmierer zuerst dachte: "Ich muss hier schlafen", und dann fragte ich mich, wie lange und unter welcher Bedingung ich aufhören sollte. Und im richtigen Fall hätte er nicht einmal einen ersten Gedanken haben sollen. Der Gedanke "sollte ein Ereignis erwarten" sollte in meinem Kopf aufgetaucht sein - und von diesem Moment an hätte der Gedanke bereits darauf hingearbeitet, den richtigen Mechanismus für die Datensynchronisierung zwischen Threads zu wählen, wodurch sowohl die boolesche Variable als auch die Verwendung von SleepEx eliminiert würden.

    Schlechtes Beispiel Nummer 4


    In diesem Beispiel werfen wir einen Blick auf die Funktion backupDocument, die als "autosave" dient und bei einem unerwarteten Absturz des Editors hilfreich ist. Standardmäßig schläft sie für 7 Sekunden und gibt dann den Befehl zum Speichern der Änderungen (falls vorhanden).

    DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/)
    {
        ...
        while (isSnapshotMode)
        {
            ...
            ::Sleep(DWORD(timer));
            ...
            ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0);
        }
        return TRUE;
    }

    Das Intervall ist veränderbar, aber nicht in dieser Schwierigkeit. Jeder Abstand ist zu groß und zu klein. Wenn wir einen Buchstaben pro Minute eingeben, hat es keinen Sinn, nur 7 Sekunden zu schlafen. Wenn wir 10 Megabytes Text von irgendwo kopieren und einfügen - es ist nicht nötig, weitere 7 Sekunden danach zu warten, ist er groß genug, um sofort ein Backup zu starten (plötzlich haben wir es von irgendwo abgeschnitten und waren nicht da, und der Editor stürzt in einer Sekunde ab).

    Ie mit einfacher Erwartung ersetzen wir hier den fehlenden intelligenteren Algorithmus.

    Schlechtes Beispiel Nummer 5


    Notepad ++ kann "Text eingeben" - d. H. Emulieren Sie die Texteingabe einer Person und pausieren Sie zwischen den Buchstaben. Es scheint als "Osterei" geschrieben zu sein, aber Sie können sich eine funktionierende Anwendung dieser Funktion vorstellen ( Narr Upwork, ja ).

    int pauseTimeArray[nbPauseTime] = {200,400,600};
    constint maxRange = 200;
    ...
    int ranNum = getRandomNumber(maxRange);
    ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]);
    ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0);

    Das Problem hierbei ist, dass der Code eine Vorstellung von einer Art "durchschnittlicher Person" enthält, die 400-800 ms zwischen den gedrückten Tasten pausiert. Ok, vielleicht ist es "durchschnittlich" und normal. Aber wissen Sie, wenn das Programm, das ich benutze, einige Pausen in meiner Arbeit macht, nur weil sie schön und angemessen für sie erscheinen, bedeutet das keineswegs, dass ich ihre Meinung teile. Ich möchte die Dauer dieser Pausen anpassen können. Wenn dies im Fall von Notepad ++ nicht sehr kritisch ist, stieß ich in anderen Programmen manchmal auf Einstellungen wie "Daten aktualisieren: häufig, normal, selten", bei denen "oft" für mich nicht oft genug und "selten" nicht ausreichend waren selten Ja und "normal" war nicht normal. Diese Funktionalität sollte dem Benutzer die Möglichkeit geben, die Anzahl der Millisekunden genau anzugeben. die er warten möchte, bevor er die gewünschte Aktion ausführt. Bei der obligatorischen Möglichkeit, "0" einzugeben. Darüber hinaus sollte 0 in diesem Fall nicht einmal als Argument an die Sleep-Funktion übergeben werden, sondern einfach den Aufruf ausschließen (Sleep (0) kehrt nicht sofort zurück, sondern gibt den verbleibenden Teil des vom Scheduler ausgegebenen Zeitfensters an einen anderen Thread zurück).

    Schlussfolgerungen


    Mit Hilfe des Sleep-Modus ist es möglich und notwendig, zu warten, wenn es sich um ein bedingungslos definiertes Warten auf einen bestimmten Zeitraum handelt und es eine logische Erklärung dafür gibt, warum es so ist: „Nach dem technischen Prozess“, „Zeit wird anhand dieser Formel berechnet“, „So viel Warten sagte der Kunde. " Das Warten auf bestimmte Ereignisse oder die Synchronisation von Threads sollte nicht mit der Sleep-Funktion implementiert werden.

    Jetzt auch beliebt: