Comment bien dormir et bien dormir

Il n'y a pas si longtemps, un bon article nous est parvenu sur le terrible état de performance des logiciels modernes (l' original en anglais , traduction en Habré ). Cet article m'a rappelé un contre-modèle du code, qui est très commun et fonctionne généralement d'une manière ou d'une autre, mais conduit à de petites pertes de performances ici et là. Eh bien, vous savez, une bagatelle, que les mains n'atteindront en aucune façon. Le seul problème est qu'une douzaine de ces "bagatelles" dispersées à différents endroits du code commencent à poser des problèmes tels que "J'ai le dernier Intel Core i7 et les secousses de défilement".

Je parle de l'utilisation incorrecte de la fonction veille (le cas peut varier en fonction du langage de programmation et de la plate-forme). Alors qu'est-ce que le sommeil? La documentation répond très simplement à cette question: il s'agit d'une pause dans l'exécution du thread courant pendant le nombre de millisecondes spécifié. Il convient de noter la beauté esthétique du prototype de cette fonction:

void Sleep(DWORD dwMilliseconds); 

Un seul paramètre (extrêmement clair), aucun code d'erreur ni exception - cela fonctionne toujours. Il y a très peu de fonctions aussi agréables et compréhensibles!

Vous obtenez encore plus de respect pour cette fonction lorsque vous lisez son fonctionnement.
La fonction va au planificateur de threads du système d'exploitation et lui dit: «Mon thread et moi aimerions refuser le temps CPU qui nous est alloué, maintenant et pour autant de millisecondes à l'avenir. Donnez aux pauvres! " L'ordonnanceur, légèrement surpris par une telle générosité, supprime les fonctions de gratitude au nom du processeur, donne le temps restant à la personne suivante (et il y en a toujours) et n'inclut pas le thread qui a fait que Sleep a été prétendu pour transmettre le contexte d'exécution pendant le nombre spécifié de millisecondes. La beauté!

Qu'est-ce qui aurait pu mal tourner? Le fait que les programmeurs utilisent cette merveilleuse fonctionnalité n'est pas pour ce à quoi elle est destinée.

Et il est destiné à la simulation logicielle de certains processus externes, définis par quelque chose de réel, de pause.

Exemple correct numéro 1


Nous écrivons l'application «horloge», dans laquelle une fois par seconde vous devez changer le nombre sur l'écran (ou la position de la flèche). La fonction Sleep ici est parfaitement adaptée: nous n'avons vraiment rien à faire pendant une période de temps clairement définie (exactement une seconde). Pourquoi ne pas dormir?

Exemple correct numéro 2


Nous écrivons un contrôleur de lune pour une machine à pain. L'algorithme d'opération est défini par l'un des programmes et ressemble à ceci:

  1. Passez en mode 1.
  2. Travaillez dedans pendant 20 minutes
  3. Passez en mode 2.
  4. Travaillez dedans pendant 10 minutes
  5. Éteignez.

Ici aussi, tout est clair: nous travaillons avec le temps, il est déterminé par le processus technologique. Utiliser Sleep est acceptable.

Voyons maintenant des exemples d'utilisation abusive du sommeil.

Lorsque j'ai besoin d'un exemple de code C ++ incorrect, je vais dans le référentiel de code de l'éditeur de texte Notepad ++. Son code est si terrible que tout anti-modèle est définitivement là, j'ai même écrit un article à ce sujet une fois. Notepad ++ ne m'a pas déçu cette fois non plus! Voyons comment il utilise Sleep.

Mauvais exemple numéro 1


Au démarrage, Notepad ++ vérifie si une autre instance de son processus est déjà en cours d'exécution et, si tel est le cas, recherche sa fenêtre et lui envoie un message, puis se ferme. Pour détecter un autre processus, une méthode standard est utilisée - le mutex global nommé. Mais le code suivant a été écrit pour rechercher des fenêtres:

 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) { ... } ... } 


Le programmeur qui a écrit ce code a essayé de trouver une fenêtre pour Notepad ++ qui était déjà lancée et envisageait même une situation où deux processus étaient démarrés littéralement en même temps, donc le premier d'entre eux a déjà créé un mutex global, mais n'a pas encore créé de fenêtre d'éditeur. Dans ce cas, le deuxième processus attendra la création de la fenêtre "5 fois en 100 ms". Par conséquent, soit nous n'attendrons pas du tout, soit nous perdrons jusqu'à 100 ms entre le moment de la création réelle de la fenêtre et la sortie de Sleep.

C'est le premier (et l'un des principaux) contre-modes d'utilisation du sommeil. Nous n'attendons pas l'occurrence de l'événement, mais "pendant quelques millisecondes, il aura soudain de la chance". Nous attendons tellement que, d'une part, nous n'ennuyons pas vraiment l'utilisateur, et d'autre part, nous ayons la chance d'attendre l'événement dont nous avons besoin. Oui, l'utilisateur peut ne pas remarquer de pause de 100 ms lors du démarrage de l'application. Mais si une telle pratique consistant à «attendre un peu du bulldozer» est acceptée et acceptable dans le projet, elle peut se terminer par le fait que nous attendrons à chaque étape pour les raisons les plus mesquines. Ici 100 ms, il y a encore 50 ms, et ici 200 ms - et ici notre programme "ralentit en quelque sorte pendant quelques secondes".

De plus, il est simplement esthétiquement désagréable de voir du code qui fonctionne pendant longtemps alors qu'il pourrait fonctionner rapidement. Dans ce cas particulier, on pourrait utiliser la fonction SetWindowsHookEx , en s'abonnant à l'événement HSHELL_WINDOWCREATED - et recevoir instantanément une notification de création de fenêtre. Oui, le code devient un peu plus compliqué, mais littéralement 3-4 lignes. Et nous gagnons jusqu'à 100 ms! Et le plus important - nous n'utilisons plus les fonctions de l'attente inconditionnelle où l'attente n'est pas inconditionnelle.

Mauvais exemple numéro 2


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

Je ne comprenais pas vraiment exactement et pendant combien de temps ce code attendait dans Notepad ++, mais je voyais souvent l'anti-modèle général "démarrer le flux et attendre". Les gens attendent des choses différentes: le début d'un autre flux, la réception de certaines données de celui-ci, la fin de son travail. Deux choses sont mauvaises ici tout de suite:

  1. La programmation multithread est nécessaire pour faire quelque chose de multithread. C'est-à-dire le lancement du deuxième thread suppose que nous continuerons à faire quelque chose dans le premier, à ce moment-là, le deuxième thread fera d'autres travaux, et le premier, ayant terminé son travail (et, peut-être, ayant attendu un peu plus), obtiendra son résultat et l'utilisera d'une manière ou d'une autre. Si nous commençons à "dormir" immédiatement après le démarrage du deuxième thread - pourquoi est-il nécessaire?
  2. Il faut s'y attendre correctement. Pour une attente correcte, il existe des pratiques éprouvées: l'utilisation d'événements, de fonctions d'attente, d'appels de rappel. Si nous attendons que le code commence à fonctionner dans le deuxième thread, configurez un événement pour cela et signalez-le dans le deuxième thread. Si nous attendons que le deuxième thread finisse de fonctionner - en C ++, il existe une merveilleuse classe de threads et sa méthode de jointure (enfin, ou, encore une fois, des méthodes spécifiques à la plate-forme comme WaitForSingleObject et HANDLE sous Windows). Attendre que le travail soit terminé dans un autre thread "pendant quelques millisecondes" est tout simplement stupide, car si nous n'avons pas de système d'exploitation en temps réel, personne ne vous donnera aucune garantie pour combien de temps ce deuxième thread démarrera ou atteindra une étape de son travail.

Mauvais exemple numéro 3


Ici, nous voyons un fil d'arrière-plan qui dort en attendant certains événements.

 class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; }; 

Je dois admettre que ce n'est pas Sleep qui est utilisé ici, mais SleepEx , qui est plus intelligent et peut interrompre l'attente de certains événements (comme la fin d'opérations asynchrones). Mais cela n'aide pas du tout! Le fait est que la boucle while (! M_bTerminate) a le droit de fonctionner sans fin, ignorant la méthode RequestTermination () appelée à partir d'un autre thread, réinitialisant la variable m_bTerminate à true. J'ai écrit sur les causes et les conséquences de cela dans un article précédent . Pour éviter cela, vous devez utiliser quelque chose garanti pour fonctionner correctement entre les threads: atomique, événement ou quelque chose de similaire.

Oui, formellement SleepEx n'est pas à blâmer pour le problème de l'utilisation de la variable booléenne habituelle pour synchroniser les threads, il s'agit d'une erreur distincte d'une autre classe. Mais pourquoi est-ce devenu possible dans ce code? Parce qu'au début, le programmeur a pensé «vous devez dormir ici», puis a réfléchi à la durée et aux conditions pour arrêter de le faire. Et dans le bon scénario, il ne devrait même pas avoir une première pensée. La pensée «devrait attendre un événement» aurait dû surgir dans ma tête - et à partir de maintenant, la pensée aurait fonctionné pour choisir le bon mécanisme de synchronisation des données entre les threads, ce qui exclurait à la fois la variable booléenne et l'utilisation de SleepEx.

Mauvais exemple numéro 4


Dans cet exemple, nous allons examiner la fonction backupDocument, qui agit comme une «sauvegarde automatique», utile en cas de plantage inattendu de l'éditeur. Par défaut, elle dort pendant 7 secondes, puis donne la commande pour enregistrer les modifications (si elles l'ont été).

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

L'intervalle peut être modifié, mais ce n'est pas le problème. Tout intervalle sera trop long et trop court en même temps. Si nous tapons une lettre par minute, cela n'a aucun sens de dormir pendant seulement 7 secondes. Si nous copions-collons 10 mégaoctets de texte quelque part, nous n'avons pas à attendre 7 secondes après cela, c'est un volume suffisamment important pour lancer immédiatement la sauvegarde (tout d'un coup, nous la coupons de quelque part et elle est partie, et l'éditeur se bloque après une seconde).

C'est-à-dire avec une simple attente, nous remplaçons ici l'algorithme plus intelligent manquant.

Mauvais exemple numéro 5


Notepad ++ peut "taper du texte" - c'est-à-dire émuler la saisie de texte humain en faisant une pause entre l'insertion de lettres. Il semble être écrit comme un "œuf de Pâques", mais vous pouvez trouver une sorte d'application fonctionnelle de cette fonctionnalité ( duper Upwork, oui ).

 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); 

Le problème ici est que le code a une idée d'une sorte de «personne moyenne» faisant une pause de 400 à 800 ms entre chaque touche enfoncée. Ok, c'est peut-être "moyen" et normal. Mais vous savez, si le programme que j'utilise fait des pauses dans mon travail simplement parce qu'ils semblent beaux et appropriés pour elle - cela ne signifie pas du tout que je partage son avis. Je voudrais pouvoir ajuster la durée des données de pause. Et, si dans le cas de Notepad ++ ce n'est pas très critique, alors dans d'autres programmes, je suis parfois tombé sur des paramètres tels que «mise à jour des données: souvent, normalement, rarement», où «souvent» ne me suffisait pas souvent, et «rarement» ne suffisait pas rarement. Oui, et «normal» n'était pas normal. Cette fonctionnalité doit permettre à l'utilisateur d'indiquer avec précision le nombre de millisecondes qu'il souhaite attendre jusqu'à ce que l'action souhaitée soit terminée. Avec l'option obligatoire pour entrer "0". De plus, 0 dans ce cas ne doit même pas être passé comme argument à la fonction Sleep, mais simplement exclure son appel (Sleep (0) ne retourne pas réellement instantanément, mais donne la partie restante de la tranche de temps donnée par le planificateur à un autre thread).

Conclusions


Avec l'aide du sommeil, on peut et doit répondre à une attente quand il s'agit d'une attente assignée inconditionnellement pour une période de temps spécifique et il y a une explication logique pour laquelle il en est ainsi: «selon le processus technologique», «le temps est calculé selon cette formule», «attendez tellement dit le client. " L'attente de certains événements ou la synchronisation des threads ne doivent pas être implémentées à l'aide de la fonction Veille.

Source: https://habr.com/ru/post/fr427843/


All Articles