Vor nicht allzu langer Zeit lief ein guter Artikel über den schrecklichen Leistungszustand moderner Software (
Original in Englisch ,
Übersetzung in Habré ) an uns vorbei. Dieser Artikel erinnerte mich an ein Antimuster des Codes, das sehr häufig ist und im Allgemeinen irgendwie funktioniert, aber hier und da zu kleinen Leistungsverlusten führt. Weißt du, eine Kleinigkeit, die die Hände in keiner Weise erreichen werden. Das einzige Problem ist, dass ein Dutzend dieser "Kleinigkeiten", die an verschiedenen Stellen im Code verstreut sind, zu Problemen wie "Ich habe den neuesten Intel Core i7 und das Scrollen zuckt" führen.

Ich spreche von der falschen Verwendung der
Sleep- Funktion (der Fall kann je nach Programmiersprache und Plattform variieren). Was ist Schlaf? Die Dokumentation beantwortet diese Frage sehr einfach: Dies ist eine Pause in der Ausführung des aktuellen Threads für die angegebene Anzahl von Millisekunden. Es sei auf die ästhetische Schönheit des Prototyps dieser Funktion hingewiesen:
void Sleep(DWORD dwMilliseconds);
Nur ein Parameter (extrem klar), keine Fehlercodes oder Ausnahmen - es funktioniert immer. Es gibt nur sehr wenige so nette und verständliche Funktionen!
Sie erhalten noch mehr Respekt vor dieser Funktion, wenn Sie lesen, wie sie funktioniert.Die Funktion geht zum OS-Thread-Scheduler und teilt ihm mit: „Mein Thread und ich möchten die uns zugewiesene CPU-Zeit jetzt und in Zukunft für ebenso viele Millisekunden ablehnen. Gib den Armen! " Der Scheduler, der von dieser Großzügigkeit leicht überrascht ist, nimmt die Funktionen der Dankbarkeit im Namen des Prozessors heraus, gibt der nächsten Person die verbleibende Zeit (und es gibt immer solche) und enthält nicht den Thread, der dazu geführt hat, dass Sleep so getan hat, als würde er den Ausführungskontext für die angegebene Anzahl von Millisekunden an ihn übertragen. Schönheit!
Was hätte schief gehen können? Die Tatsache, dass Programmierer diese wunderbare Funktion verwenden, ist nicht das, wofür sie gedacht ist.
Und es ist für die Software-Simulation eines externen, durch einen realen Pausenprozess definierten Prozesses vorgesehen.
Richtiges Beispiel Nummer 1
Wir schreiben eine "Uhr" -Anwendung, in der Sie einmal pro Sekunde eine Ziffer auf dem Bildschirm (oder die Position des Pfeils) ändern müssen. Die Schlaffunktion ist hier perfekt geeignet: Wir haben für einen klar definierten Zeitraum (genau eine Sekunde) wirklich nichts zu tun. Warum nicht schlafen?
Richtiges Beispiel Nummer 2
Wir schreiben einen
Mondschein- Controller
für eine Brotmaschine. Der Operationsalgorithmus wird von einem der Programme eingestellt und sieht ungefähr so aus:
- Gehen Sie zu Modus 1.
- 20 Minuten darin arbeiten
- Gehen Sie zu Modus 2.
- 10 Minuten darin arbeiten
- Ausschalten.
Auch hier ist alles klar: Wir arbeiten mit der Zeit, sie wird durch den technologischen Prozess bestimmt. Die Verwendung von Schlaf ist akzeptabel.
Schauen wir uns nun Beispiele für den Missbrauch des Schlafes an.
Wenn ich ein Beispiel für falschen C ++ - Code benötige, gehe ich zum Code-
Repository des Notepad ++ - Texteditors. Sein Code ist so schrecklich, dass definitiv jedes Antimuster vorhanden ist. Ich habe sogar einmal
einen Artikel darüber geschrieben. Notepad ++ hat mich auch diesmal nicht enttäuscht! Mal sehen, wie es Schlaf verwendet.
Schlechtes Beispiel Nummer 1
Beim Start prüft Notepad ++, ob eine andere Instanz seines Prozesses bereits ausgeführt wird, sucht in diesem Fall nach seinem Fenster, sendet ihm eine Nachricht und schließt sich selbst. Um einen anderen Prozess zu erkennen, wird eine Standardmethode verwendet - der global benannte Mutex. Der folgende Code wurde jedoch geschrieben, um nach Windows 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 bereits gestartete Fenster von Notepad ++ zu finden, und sich sogar eine Situation vorgestellt, in der zwei Prozesse buchstäblich gleichzeitig gestartet wurden. Der erste von ihnen hat bereits einen globalen Mutex erstellt, aber noch kein Editorfenster erstellt. In diesem Fall wartet der zweite Prozess auf die Erstellung des Fensters „5 Mal in 100 ms“. Infolgedessen werden wir entweder überhaupt nicht warten oder bis zu 100 ms zwischen dem Zeitpunkt der tatsächlichen Erstellung des Fensters und dem Verlassen des Ruhezustands verlieren.
Dies ist das erste (und eines der wichtigsten) Antimuster bei der Verwendung von Schlaf. Wir warten nicht auf das Eintreten des Ereignisses, sondern "für ein paar Millisekunden wird es plötzlich Glück haben". Wir warten so lange, dass wir einerseits den Benutzer nicht wirklich nerven und andererseits die Chance haben, auf das Ereignis zu warten, das wir brauchen. Ja, der Benutzer bemerkt möglicherweise keine Pause von 100 ms, wenn er die Anwendung startet. Aber wenn eine solche Praxis, „ein wenig vom Bulldozer zu warten“, im Projekt akzeptiert und akzeptiert wird, kann dies damit enden, dass wir aus den kleinsten Gründen bei jedem Schritt warten werden. Hier 100 ms, hier noch 50 ms und hier 200 ms - und hier verlangsamt sich unser Programm bereits "irgendwie für ein paar Sekunden".
Darüber hinaus ist es einfach ästhetisch unangenehm, Code zu sehen, der lange läuft und schnell funktionieren kann. In diesem speziellen Fall könnte man die
SetWindowsHookEx- Funktion verwenden, um das Ereignis
HSHELL_WINDOWCREATED zu abonnieren - und sofort eine Benachrichtigung über die Fenstererstellung erhalten. 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 der bedingungslosen Erwartung nicht mehr verwenden, wenn die Erwartung nicht unbedingt 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 dieser Code in Notepad ++ gewartet hat, aber ich habe oft das allgemeine Antipattern "Stream starten und warten" gesehen. Die Leute erwarten verschiedene Dinge: den Beginn eines anderen Streams, den Empfang einiger Daten von ihm, das Ende seiner Arbeit. Zwei Dinge sind hier sofort schlecht:
- Multithread-Programmierung ist erforderlich, um etwas Multithread-Programm zu erstellen. Das heißt, Beim Start des zweiten Threads wird davon ausgegangen, dass wir im ersten Thread weiterhin etwas tun werden. Zu diesem Zeitpunkt wird der zweite Thread andere Arbeiten ausführen, und der erste, der seine Arbeit beendet hat (und möglicherweise etwas länger gewartet hat), wird sein Ergebnis erhalten und es irgendwie verwenden. Wenn wir sofort nach dem Start des zweiten Threads anfangen zu "schlafen" - warum wird es überhaupt benötigt?
- Es ist notwendig, richtig zu erwarten. Für die richtige Erwartung gibt es bewährte Methoden: die Verwendung von Ereignissen, Wartefunktionen und Rückrufaufrufen. Wenn wir darauf warten, dass der Code im zweiten Thread funktioniert, richten Sie hierfür ein Ereignis ein und signalisieren Sie es im zweiten Thread. Wenn wir darauf warten, dass der zweite Thread funktioniert - in C ++ gibt es eine wunderbare Thread-Klasse und ihre Join-Methode (oder auch plattformspezifische Methoden wie WaitForSingleObject und HANDLE unter Windows). Es ist einfach dumm, „einige Millisekunden“ darauf zu warten, dass die Arbeit in einem anderen Thread abgeschlossen ist, denn wenn wir kein Echtzeit-Betriebssystem haben, gibt Ihnen niemand eine Garantie dafür, wie lange dieser zweite Thread startet oder ein Stadium seiner Arbeit erreicht.
Schlechtes Beispiel Nummer 3
Hier sehen wir einen Hintergrund-Thread, der schläft und auf einige Ereignisse wartet.
class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; };
Ich muss zugeben, dass hier nicht Sleep verwendet wird, sondern
SleepEx , das intelligenter ist und das Warten auf bestimmte Ereignisse (z. B. den Abschluss asynchroner Vorgänge) unterbrechen kann. Das hilft aber überhaupt nicht! Tatsache ist, dass die while-Schleife (! M_bTerminate) das Recht hat, endlos zu arbeiten. Dabei wird die von einem anderen Thread aufgerufene RequestTermination () -Methode ignoriert und die Variable m_bTerminate auf true zurückgesetzt. Ich habe in einem
früheren Artikel über die Ursachen und Folgen davon geschrieben. Um dies zu vermeiden, sollten Sie etwas verwenden, das garantiert korrekt zwischen Threads funktioniert: Atomic, Event oder ähnliches.
Ja, formal ist SleepEx nicht für das Problem verantwortlich, die übliche boolesche Variable zum Synchronisieren von Threads zu verwenden. Dies ist ein separater Fehler einer anderen Klasse. Aber warum wurde es in diesem Code möglich? Denn zuerst dachte der Programmierer: „Du musst hier schlafen“ und dachte dann darüber nach, wie lange und unter welchen Bedingungen er damit aufhören soll. Und im richtigen Szenario sollte er nicht einmal einen ersten Gedanken haben. Der Gedanke „müsste auf ein Ereignis warten“ sollte in meinem Kopf aufgetaucht sein - und von nun an hätte der Gedanke darauf hingearbeitet, den richtigen Mechanismus für die Synchronisierung von Daten zwischen Threads auszuwählen, der sowohl die boolesche Variable als auch die Verwendung von SleepEx ausschließen würde.
Schlechtes Beispiel Nummer 4
In diesem Beispiel sehen wir uns die Funktion backupDocument an, die als "automatische Speicherung" fungiert und bei einem unerwarteten Absturz des Editors hilfreich ist. Standardmäßig schläft sie 7 Sekunden lang und gibt dann den Befehl zum Speichern der Änderungen (falls vorhanden).
DWORD WINAPI Notepad_plus::backupDocument(void * ) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; }
Das Intervall kann geändert werden, dies ist jedoch nicht das Problem. Jedes Intervall ist gleichzeitig zu lang und zu kurz. Wenn wir einen Buchstaben pro Minute eingeben, macht es keinen Sinn, nur 7 Sekunden zu schlafen. Wenn wir 10 Megabyte Text von irgendwoher kopieren und einfügen, müssen wir danach keine weiteren 7 Sekunden warten. Es ist groß genug, um die Sicherung sofort zu starten (plötzlich schneiden wir es von irgendwoher aus und es ist dort weg und der Editor stürzt nach einer Sekunde ab).
Das heißt, Mit einfacher Erwartung ersetzen wir hier den fehlenden intelligenteren Algorithmus.
Schlechtes Beispiel Nummer 5
Notepad ++ kann "Text eingeben" - d. H. Emulieren Sie die Eingabe von menschlichem Text, indem Sie zwischen dem Einfügen von Buchstaben eine Pause einlegen. Es scheint als "Osterei" geschrieben zu sein, aber Sie können sich eine funktionierende Anwendung dieser Funktion
einfallen lassen (
Upwork täuschen, ja ).
int pauseTimeArray[nbPauseTime] = {200,400,600}; const int 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“ hat, die zwischen jeder gedrückten Taste für 400-800 ms pausiert. Ok, vielleicht ist es "durchschnittlich" und normal. Aber weißt du, wenn das Programm, das ich benutze, einige Pausen in meiner Arbeit macht, nur weil sie schön und für sie geeignet erscheinen - das bedeutet überhaupt nicht, dass ich ihre Meinung teile. Ich möchte die Dauer der Pausendaten anpassen können. Und wenn dies im Fall von Notepad ++ nicht sehr kritisch ist, stieß ich in anderen Programmen manchmal auf Einstellungen wie „Daten aktualisieren: oft, normalerweise, selten“, wobei „oft“ für mich oft nicht genug und „selten“ nicht genug war selten. Ja, und "normal" war nicht normal. Diese Funktionalität sollte es dem Benutzer ermöglichen, die Anzahl der Millisekunden, die er warten möchte, bis die gewünschte Aktion abgeschlossen ist, genau anzugeben. Mit der obligatorischen Option zur Eingabe von "0". Darüber hinaus sollte 0 in diesem Fall nicht einmal als Argument an die Sleep-Funktion übergeben werden, sondern einfach ihren Aufruf ausschließen (Sleep (0) kehrt nicht sofort zurück, sondern gibt den verbleibenden Teil des vom Scheduler angegebenen Zeitfensters an einen anderen Thread weiter).
Schlussfolgerungen
Mit Hilfe des Schlafes ist es möglich und notwendig, eine Erwartung zu erfüllen, wenn es sich genau um eine bedingungslos zugewiesene Erwartung für einen bestimmten Zeitraum handelt, und es gibt eine logische Erklärung dafür, warum dies so ist: „Nach der Prozesstechnologie“, „Zeit wird nach dieser Formel berechnet“, „so viel warten sagte der Kunde. " Das Warten auf bestimmte Ereignisse oder das Synchronisieren von Threads sollte nicht mit der Sleep-Funktion implementiert werden.